use case · static sites
Stop form spam on static websites.
Updated May 12, 2026
Static-site frameworks — Astro, Next.js (static export), Gatsby, Hugo, Eleventy, SvelteKit (prerendered) — give you a fast, cheap site at the cost of having no built-in server. Forms on those sites either point at a third-party form-handler (Formspree, Basin) or a single serverless function you run yourself. Either way, the function is the right place to classify, and Siftfy fits in two lines of code.
Why "static + one function" beats third-party form handlers
Drop-in form handlers like Formspree are convenient but they handle spam by either showing a CAPTCHA (friction) or pattern-matching subject lines (weak). They also make every form submission a third-party data flow — the message body lives on a vendor's server before it reaches your inbox. Owning a 30-line serverless function gives you full control of the data path and lets you swap in a real classifier instead of a regex.
On Vercel and Netlify the function runs at the edge with no cold start tax for low traffic, and the entire round-trip — browser → edge function → Siftfy → email — typically completes in under 250 ms.
One-function reference
The function below is a Next.js App Router route, but the only framework-specific lines are the POST handler signature and the NextResponse import. The same logic ports cleanly to:
- Astro:
src/pages/api/contact.tswithexport const POSTand aResponsereturn. - Netlify Functions:
netlify/functions/contact.ts,handlerexport. - Cloudflare Pages Functions:
functions/api/contact.ts,onRequestPostexport. - SvelteKit:
+server.tswithexport async function POST. - Hugo / Jekyll / Eleventy: these have no functions concept — point your form at a Cloudflare Worker (see Webflow pattern) or any of the above platforms.
// app/api/contact/route.ts — Next.js App Router on Vercel.
// Same shape works as a Netlify Function, Astro server endpoint,
// SvelteKit +server route, or Cloudflare Pages Function.
import { NextResponse } from "next/server";
const SPAM_THRESHOLD = 0.85;
const QUEUE_THRESHOLD = 0.5;
export async function POST(req: Request) {
const { email, message } = await req.json();
if (!email || !message) {
return NextResponse.json({ error: "missing fields" }, { status: 400 });
}
let probability = 0;
try {
const resp = await fetch("https://api.siftfy.io/v1/predict", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.SIFTFY_KEY!,
},
body: JSON.stringify({ text: message }),
signal: AbortSignal.timeout(2000),
});
if (resp.ok) ({ spam_probability: probability } = await resp.json());
} catch {
// Fall open on transport failure.
}
if (probability >= SPAM_THRESHOLD) {
// Always 200; don't tell the spammer they were caught.
return NextResponse.json({ ok: true });
}
if (probability >= QUEUE_THRESHOLD) {
await fetch(process.env.QUEUE_WEBHOOK!, {
method: "POST",
body: JSON.stringify({ email, message, probability }),
headers: { "Content-Type": "application/json" },
});
return NextResponse.json({ ok: true });
}
await sendEmail({ to: process.env.INBOX!, from: email, body: message });
return NextResponse.json({ ok: true });
}Hooking the form to the function
Plain HTML form, no client JS:
<form action="/api/contact" method="POST" enctype="application/json">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>With a tiny client-side handler (so the user stays on-page after submitting) the same flow works behind a fetch call; the function never knows the difference. Either way, classification happens on the server side where the score doesn't travel back to the browser for spammers to inspect.
Edge cases worth handling
- Cold starts. First-call cold starts on lightly-trafficked functions can add 200-400 ms. Set a 2-second timeout on the Siftfy call so a slow path never breaks the form UX.
- Multiple form types. A contact form and a job-application form have different cost-of-false-positive — be willing to lose a contact lead but never lose a job applicant. Branch on the form type and use different thresholds.
- Bot-only honeypot. Add a hidden
websitefield that real users won't fill in and that bots usually do. Reject anything where it's non-empty before calling Siftfy — saves a request quota and is a free extra signal. - Don't reveal the score. Always return 200 to the form, even on a hard block. The user sees "thanks" and the spammer doesn't learn the threshold.
10,000 submissions / month free. Read the /v1/predict reference, or peek at related use cases: contact forms, Webflow, headless CMS.