Integration guide · Next.js
A Next.js contact form that emails you — with or without an API route
A contact form in Next.js doesn't need its own API route, database, or email setup. Point a plain HTML form at a PostTo endpoint and you're done — it works in server components with zero client-side JavaScript.
Want more? Add a route handler that forwards submissions through PostTo's HMAC-signed API: the signing secret stays in a server-side environment variable, and every request is protected against replay and forgery.
Free plan, no credit card required.
Three steps, a few minutes
Create an endpoint
Sign up free, create an endpoint in the dashboard, and set your destination email. You'll get an endpoint URL right away — and a signing secret if you enable signed mode.
Pick your integration
Simplest: a plain HTML form in any page or server component. More control: a route handler that signs the request server-side and lets you render inline success states.
Deploy anywhere
Vercel, Netlify, a container — PostTo is a plain HTTPS endpoint, so nothing about your hosting changes. Submissions land in your inbox and dashboard.
The code
Replace YOUR_TOKEN
with the endpoint URL from your dashboard.
export default function ContactPage() {
return (
<form action="https://postto.dev/api/v1/send/YOUR_TOKEN" method="POST">
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" required />
{/* Honeypot: bots fill this in, humans never see it */}
<input type="text" name="_hp" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
{/* Where to send the visitor after a successful submission */}
<input type="hidden" name="_next" value="https://yoursite.com/thanks" />
<button type="submit">Send</button>
</form>
);
}
import crypto from 'node:crypto';
export async function POST(request: Request) {
const ts = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify(await request.json());
const sig = crypto
.createHmac('sha256', process.env.POSTTO_SECRET!)
.update(`${ts}.${body}`)
.digest('hex');
const res = await fetch('https://postto.dev/api/v1/send/YOUR_TOKEN', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-PostTo-Timestamp': ts,
'X-PostTo-Signature': sig,
},
body, // exact bytes that were signed
});
return Response.json(await res.json(), { status: res.status });
}
Good to know
The plain form needs no client JS
The first snippet is a server component with a native HTML form — no "use client", no onSubmit handler, no fetch. The browser posts directly to PostTo and follows the redirect to your thanks page.
Keep the secret out of the browser
Signed mode requires your endpoint's secret key, so it belongs in a route handler or server action only. Store it as POSTTO_SECRET — never with a NEXT_PUBLIC_ prefix, which would bundle it into client code.
Spam protection is already wired in
The hidden _hp field is a honeypot — humans never see it, bots fill it in, and PostTo silently discards those submissions. Server-side baseline filtering (heuristics and optional AI classification) runs on every submission after that, so you don't need a CAPTCHA to start.
From the docs
Frequently asked questions
- Does this work with static export (output: "export")?
- The plain HTML form does — it's just markup, so it survives static export unchanged. The signed route handler needs a server runtime, so use it on deployments with server functions (Vercel, Node, etc.).
- When should I use signed mode instead of the plain form?
- Use the plain form for public contact forms — the endpoint token plus spam filtering is the right level of protection. Use signed mode when submissions come from your own server and you want cryptographic proof they weren't forged or replayed.
- Can I show an inline success message instead of redirecting?
- Yes. Post to PostTo (or your own route handler) with fetch and an Accept: application/json header, and render the JSON response — a client component with a small submit handler is all it takes.
- What does it cost?
- The free plan includes 50 submissions per month with the full dashboard — no credit card required. Paid plans start at $9/month.
Add a form to your Next.js site today
Create an endpoint, paste the snippet, and see your first submission in the dashboard — all on the free plan.
Try PostTo free