Next.js + MailKite
next-mailkite is the official MailKite package for Next.js — a signature-verified
App Router route handler for inbound email, plus a sendEmail() re-export of the
MailKite SDK. Next.js has no swappable "mailer" concept, so this is a webhook-consumer-and-sender
package, not a transport driver.
What you need
- A verified domain with SPF + DKIM published
- Your API key (
mk_live_…) - A Next.js 13.4+ project using the App Router
Install
npm install next-mailkite Receive email — an auto-reply bot in two files
Create app/api/mailkite/inbound/route.ts. createMailKiteRouteHandler()
verifies the x-mailkite-signature header (HMAC-SHA256, no network call) and hands
the parsed event to your handler:
import { createMailKiteRouteHandler } from 'next-mailkite';
import handler from '@/lib/mailkite-handler';
export const { POST } = createMailKiteRouteHandler(handler); Your handler's default export runs for every verified delivery:
import { sendEmail, replyOk } from 'next-mailkite';
import type { MailKiteInboundHandler } from 'next-mailkite';
const handler: MailKiteInboundHandler = async (event) => {
const m = event.message;
if (event.type !== 'email.received' || !m) return replyOk();
await sendEmail({
from: 'bot@myapp.ai', // an address on your verified domain
to: m.from,
subject: `Re: ${m.subject ?? 'your email'}`,
inReplyTo: m.messageId,
text: 'Thanks — got your message. A human will follow up soon.',
});
return replyOk();
};
export default handler;
Point your MailKite domain's webhook URL at
https://yourapp.com/api/mailkite/inbound (dashboard → domain → Webhooks).
No handler yet? Call createMailKiteRouteHandler() with no arguments — deliveries
are still verified, logged, and acknowledged; nothing is dropped silently.
Your handler can return:
undefined/void— acknowledged withreplyOk()- a string — use
replyOk(),replySpam(),replyDrop(),replyBlockSender()for control-mode replies - a plain object — JSON-serialized
- a
Response— returned as-is - a thrown error —
500, so MailKite retries the delivery
Send email
sendEmail() is a thin wrapper over the
mailkite SDK's
send() that reads MAILKITE_API_KEY from the environment. Call it
from server actions or route handlers — never from a "use client" component:
'use server';
import { sendEmail } from 'next-mailkite';
export async function sendInvoice(to: string) {
return sendEmail({
from: 'hello@myapp.ai',
to,
subject: 'Your invoice #1042',
html: '<p>Thanks! Receipt attached.</p>',
});
} Environment variables
# .env.local
MAILKITE_API_KEY=mk_live_...
MAILKITE_WEBHOOK_SECRET=whsec_... | Variable | Used by | Where to get it |
|---|---|---|
MAILKITE_WEBHOOK_SECRET | the route handler (signature verification) | dashboard → Webhooks |
MAILKITE_API_KEY | sendEmail() | dashboard → API keys |
Test it
curl -X POST https://yourapp.com/api/mailkite/inbound \
-H "Content-Type: application/json" \
-H "x-mailkite-signature: t=...,v1=..." \
-d '{"type":"email.received","message":{"from":"test@example.com","to":"hello@yourdomain.com","subject":"Hello","text":"Test"}}' Runtime notes
MailKite.verifyWebhook() uses node:crypto, so the inbound route must
run on the Node.js runtime — the App Router default. If you set
export const runtime = 'edge' on the route file, signature verification breaks;
leave the runtime unset (or explicitly nodejs).
Troubleshooting
- 500 "webhook secret not configured" — set
MAILKITE_WEBHOOK_SECRETin your environment. The route refuses to process a delivery it can't verify. - 401 "invalid signature" — check the raw request body reaches the handler untouched; a body-parsing middleware ahead of the route can break signature verification, which is computed over the exact bytes MailKite sent.
- Works locally, 500s on Vercel — confirm the route isn't set to
runtime = 'edge'(see Runtime notes above).
See the next-mailkite npm package, the runnable starter, or Inbound webhooks for the full payload reference.