use case · ghost cms
Ghost CMS spam prevention.
Updated May 12, 2026
Ghost is built for serious bloggers, but it ships without spam filtering on member signups — and once spam members exist, they pollute your subscriber count, your email deliverability, and any member-only comments you've enabled. Pipe Ghost's webhook for new members through Siftfy and you have a calibrated probability before the member ever sees your content.
What signals to classify
Spam member signups don't fail in obvious ways — they look like ordinary users at first glance. The signal is in the combination of name, bio note, and any custom fields you collect. A real subscriber rarely writes a 200-word "note" describing their CBD-vape side hustle; a bot does. Concatenate the natural-language fields, ignore the email address itself (Siftfy classifies content, not addresses), and send the joined text to /v1/predict.
Webhook handler
Ghost fires a webhook for every Member created event. The handler below — deployable as a Vercel function, Netlify function, or anywhere you can run Node — classifies the new member and either deletes them via the Ghost Admin API (hard block) or tags for human review (queue). The Admin API call uses a 5-minute-TTL HS256 JWT minted from your Admin API key.
// Vercel / Netlify / Express handler — receives Ghost's
// member.added webhook, classifies the bio + name, suspends if spammy.
//
// Configure in Ghost Admin: Settings → Integrations → Add custom
// integration → "Add webhook" → Event: Member created.
import crypto from "node:crypto";
const SPAM_THRESHOLD = 0.85;
const QUEUE_THRESHOLD = 0.5;
const GHOST_ADMIN_KEY = process.env.GHOST_ADMIN_KEY!; // {id}:{secret}
const GHOST_API_URL = process.env.GHOST_API_URL!; // https://your.ghost.io
const SIFTFY_KEY = process.env.SIFTFY_KEY!;
export default async function handler(req, res) {
if (req.method !== "POST") return res.status(405).end();
// Ghost wraps the new member in member.current.
const { member: { current: m } } = req.body;
// Combine the fields a spammer fills in. Email is rarely useful as
// signal — natural-language fields are.
const text = [m.name, m.note, m.email_disabled_reason].filter(Boolean).join(" • ");
if (!text) return res.status(200).json({ ok: true });
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 }),
signal: AbortSignal.timeout(2000),
});
if (resp.ok) ({ spam_probability: probability } = await resp.json());
} catch {
return res.status(200).json({ ok: true, fell_open: true });
}
if (probability >= SPAM_THRESHOLD) {
// Mint a short-lived JWT for the Ghost Admin API and remove the member.
const token = mintGhostJWT(GHOST_ADMIN_KEY);
await fetch(`${GHOST_API_URL}/ghost/api/admin/members/${m.id}/`, {
method: "DELETE",
headers: { Authorization: `Ghost ${token}` },
});
} else if (probability >= QUEUE_THRESHOLD) {
// Tag for review; left as an exercise — Ghost has labels.
}
return res.status(200).json({ ok: true, probability });
}
function mintGhostJWT(key: string): string {
const [id, secret] = key.split(":");
const header = { alg: "HS256", typ: "JWT", kid: id };
const now = Math.floor(Date.now() / 1000);
const payload = { iat: now, exp: now + 5 * 60, aud: "/admin/" };
const enc = (o: object) => Buffer.from(JSON.stringify(o)).toString("base64url");
const data = `${enc(header)}.${enc(payload)}`;
const sig = crypto.createHmac("sha256", Buffer.from(secret, "hex"))
.update(data).digest("base64url");
return `${data}.${sig}`;
}Wiring it up in Ghost
- Ghost Admin → Settings → Integrations → Add custom integration.
- Copy the Admin API key into the deployed function's environment as
GHOST_ADMIN_KEY; copy the API URL intoGHOST_API_URL. - In the same integration, add a webhook for the
Member createdevent pointing at your function's public URL. - Add
SIFTFY_KEYto the same env. Trigger a test signup to verify.
Beyond signups
The same pattern applies to anywhere Ghost emits a webhook with user-supplied text. If you've enabled native commenting (Ghost 5+), run the comment body through Siftfy before publishing. If you use a third-party comments tool like Hyvor Talk or Cusdis, those tools typically have their own webhooks; the Siftfy hop slots in cleanly.
Don't bother classifying server-generated content (your own posts, your own newsletter blasts) — Siftfy is calibrated against user-submitted text and will sometimes flag promotional copy you wrote yourself as borderline. Use it where the input is untrusted.
10,000 webhook events / month free. Read the /v1/predict reference, or peek at related use cases: comments, signups, headless CMS.