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.

Why traditional CAPTCHAs are failing modern blogs

A CAPTCHA asks whether the visitor looks like an automated session. Ghost spam usually asks a different question: does this member profile or signup note contain low-quality promotional content? Modern bots can use real browsers, while real readers still hit accessibility, mobile, and privacy friction. For a Ghost publication, that tradeoff is rarely worth making the signup path harder.

Invisible, server-side classification keeps the reader experience clean. The signup UI stays unchanged, the webhook handler scores untrusted text after submission, and your thresholds decide whether to delete, tag for review, or allow the member. If you are comparing approaches, the broader spam detection API comparison and CAPTCHA alternatives guide show where API-based filtering fits.

The direct impact of spam on your blog's SEO and revenue

Spam members inflate your list, distort paid-member reporting, and can hurt deliverability if they interact badly with email workflows. If you add comments through Ghost's native feature set or a third-party comments product, spam adds a public-quality problem: unrelated links, generic AI-written replies, and thin user-generated content on pages you want search engines and readers to trust.

That is why Ghost spam prevention should sit before member activation, and before comment publication when your comment system exposes a moderation hook. Keep the obvious spam out, route borderline cases to review, and preserve the public page quality that supports subscriptions. For the search-specific mechanics, see how comment spam affects SEO and how to detect AI-generated comment spam.

How to choose the right spam protection for your tech stack

Ghost installations split into two common shapes. Hosted publications usually need a webhook endpoint that lives on Vercel, Netlify, Cloudflare Workers, or a small backend service. Self-hosted Ghost sites can use the same pattern, but may also place the classifier beside existing moderation or member-management jobs.

Choose a service that can run outside the browser, returns a calibrated score instead of only a binary verdict, and gives you a way to fail open if the network call times out. If privacy or regional compliance matters, keep the classified text limited to user-submitted fields and avoid sending unnecessary account data. The decision framework in the spam detection API buying guide covers API-vs-plugin tradeoffs in more detail.

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.

typescript
// 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

  1. Ghost Admin → Settings → Integrations → Add custom integration.
  2. Copy the Admin API key into the deployed function's environment as GHOST_ADMIN_KEY; copy the API URL into GHOST_API_URL.
  3. In the same integration, add a webhook for the Member created event pointing at your function's public URL.
  4. Add SIFTFY_KEY to 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.

Try it free

10,000 webhook events / month free. Read the /v1/predict reference, compare spam detection APIs, or peek at related use cases: comments, signups, headless CMS, Ghost example.