use case · contact forms
Stop spam in your contact form.
Updated May 12, 2026
One POST /v1/predict on the server side turns every contact-form submission into a calibrated spam probability. No CAPTCHAs, no honeypots, no client-side JavaScript that bots already routinely defeat — just a 10ms hop in your form handler.
Why server-side?
Honeypot fields and Google-CAPTCHA tokens both run in the browser, so any bot that runs a real headless browser walks past them. The things that actually distinguish spam from a real customer message live in the content — the language, the off-topic links, the urgency tropes — and that's what Siftfy classifies. Doing it on the server means the score doesn't travel back to the browser, so there's nothing for the spammer to spoof.
Drop-in handler
The pattern below uses Node + Express; the same shape works in any server framework. Three thresholds: definitely-spam (drop), maybe (queue for review), clean (deliver immediately). Wrap the API call in a short timeout and fall open on transport failures — the cost of accidentally delivering one borderline message is far lower than dropping a real customer.
// Express handler for POST /contact
import express from "express";
const app = express();
app.use(express.json());
const SIFTFY_KEY = process.env.SIFTFY_KEY!;
const SPAM_THRESHOLD = 0.85; // hard block above this
const QUEUE_THRESHOLD = 0.5; // review between QUEUE and SPAM
app.post("/contact", async (req, res) => {
const { email, message } = req.body;
if (!email || !message) {
return res.status(400).json({ error: "email and message required" });
}
// Classify the message body. We don't bother classifying the email
// address itself — Siftfy is trained on natural language, not addresses.
let probability = 0;
try {
const resp = await fetch("https://api.siftfy.io/v1/predict", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": SIFTFY_KEY,
},
body: JSON.stringify({ text: message }),
signal: AbortSignal.timeout(2000),
});
if (resp.ok) {
({ spam_probability: probability } = await resp.json());
} else if (resp.status !== 429 && resp.status !== 503) {
// 4xx other than rate-limit is our bug — log and let the message through.
console.error("siftfy error", resp.status);
}
} catch (err) {
// Timeout or network error — fall open, don't block the user.
console.error("siftfy failed open", err);
}
if (probability >= SPAM_THRESHOLD) {
// Drop on the floor. Don't tell the sender — that just trains them.
return res.status(200).json({ ok: true });
}
if (probability >= QUEUE_THRESHOLD) {
await queueForReview({ email, message, probability });
return res.status(200).json({ ok: true });
}
await deliverToInbox({ email, message });
return res.status(200).json({ ok: true });
});Picking thresholds
Contact forms tolerate higher false-positive cost than, say, an inbox filter — most real messages are legitimate, so a wrongly-blocked one is a lost lead. We default to 0.85 for hard blocks (strong evidence of spam) and 0.50 for the review queue. Tune on your own backlog: if you find clean messages in review, raise the queue threshold; if junk slips through, lower the block threshold.
Because the response is a calibrated probability, the queue threshold has a real interpretation — at 0.5, roughly half the queued messages will be spam.
Edge cases worth handling
- Timeouts. A 2-second timeout on the Siftfy call is plenty; sub-10ms is the p99. If the request times out, log it and let the message through — same logic as a payments outage. Your alerting catches the trend.
- Rate limits. A burst of submissions can cross your per-minute cap. Treat 429 as a transient error and fall open; the next-window retry will usually classify before the queued message moves on.
- Multilingual messages. The model is trained primarily on English. If your form serves other languages, raise the block threshold to avoid false positives until we ship better coverage.
- Don't reveal the score. Always return 200 even on a hard block. A 4xx with "looks like spam" tells the spammer to rephrase; a 200 tells them their message arrived. They stop tuning, and you save bandwidth.
10,000 submissions / month free. Read the /v1/predict reference, or when honeypots stop working. Related use cases: comments, signups.