URL: /products/content-filter

---
title: Content Filter
description: Stop form spam, fraud, and cold outreach at the edge. POST a submission to FormShield's /v1/check and get an allow, review, or block decision in about 40ms.
---

Contact and signup forms are a magnet for spam, fake leads, and templated cold pitches, and regex blocklists or honeypots only catch the laziest bots. You end up hand-tuning rules and still triaging junk in your inbox instead of shipping.

Content Filter is an edge-served content model that scores contact, lead, signup, and comment submissions for spam, fraud, and cold outreach. POST a submission and get back an `allow`, `review`, or `block` decision — plus per-category probabilities — in about 40ms, fast enough to run inline in your submit handler.

## When to use it

Reach for Content Filter when you have text a user typed and want a decision before you store it, send it, or notify on it.

<CardGroup cols={2}>
  <Card title="Full submission" icon="shield">
    `POST /v1/check` takes the form fields plus any IP, email, or user agent you have, and fuses content, IP, and email signals into one calibrated score.
  </Card>
  <Card title="Text only" icon="align-left">
    `POST /v1/content` scores a single block of text on its own — a comment, a message body — when that is all you have. No other metadata required.
  </Card>
</CardGroup>

Both return the same `0.0`–`1.0` score and the same per-category probabilities, so you branch on one field instead of a pile of heuristics.

## How it works

<Steps>
  <Step title="POST the submission">
    Send the form fields (and any IP, email, or user agent you have) to `POST /v1/check` with a Bearer key. For text you already have in hand, `POST /v1/content` scores the message body alone — no other metadata required.
  </Step>

  <Step title="Score at the edge">
    A content model running in the Cloudflare edge classifies the text for spam, fraud, and cold outreach, fused with IP and email signals into one calibrated `0.0`–`1.0` score. The full check returns in about 40ms, so it fits inline in your submit handler.
  </Step>

  <Step title="Act on the decision">
    You get back a `decision` of `allow`, `review`, or `block` plus per-category probabilities. `allow` saves it, `review` queues it for a human, `block` drops it — your form handler branches on one field.
  </Step>
</Steps>

## Quickstart

Send the submission to `/v1/check` from your form handler with your secret key as a Bearer token. Here a templated backlink pitch scores high and resolves to `block`.

```bash
curl -X POST https://api.formshield.dev/v1/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "form_id": "contact-form",
    "form_data": {
      "name": "John Smith",
      "email": "john@example.com",
      "message": "Boost your SEO rankings fast! Limited time backlink package, act now."
    },
    "metadata": { "ip": "203.0.113.42" }
  }'
```

The response is wrapped in the standard [envelope](/api-reference/introduction#response-envelope). The `score` is a calibrated probability in `[0,1]`, `decision` is the enum your handler branches on, and `categories` breaks the risk down by type.

```json
{
  "version": "1",
  "data": {
    "score": 0.92,
    "confidence": 0.81,
    "decision": "block",
    "categories": { "spam": 0.92, "fraud": 0.06, "cold_email": 0.41 },
    "signals": {
      "content": { "risk": 0.92, "spam_probability": 0.94, "language": "en", "flags": ["promotional_language", "urgency_markers"] }
    },
    "rule_matches": [],
    "fusion": { "method": "weighted_sum", "model_version": "v2" }
  },
  "error": null,
  "metadata": { "request_id": "req_abc123def456", "processing_time_ms": 38 }
}
```

When all you have is a block of text — a comment body, say — score it alone with `/v1/content`. It returns the same score and category probabilities without any IP or email metadata.

```bash
curl -X POST https://api.formshield.dev/v1/content \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Boost your SEO rankings fast! Limited time backlink package, act now."
  }'
```

<Note>
  `/v1/check` requires at least one of `metadata.email`, `metadata.ip`, or `form_data`. Pass the visitor's real IP and email when you have them — the more signals you send, the sharper the fused score.
</Note>

## The signals it scores

The content model returns one fused `score` and breaks the risk down into named `categories`, each a `0.0`–`1.0` probability. The `content` signal also carries human-readable `flags`.

<CardGroup cols={2}>
  <Card title="Spam and promotional content" icon="ban">
    Catches pharma, SEO and backlink pitches, gambling, crypto schemes, and templated bot copy. Surfaces human-readable flags like `promotional_language`, `urgency_markers`, and `suspicious_links` alongside a spam probability.
  </Card>
  <Card title="Cold outreach detection" icon="envelope">
    A dedicated `cold_email` category scores unsolicited sales and partnership pitches separately from outright spam, so you can block junk while routing real-but-unwanted outreach to `review` instead of silently dropping it.
  </Card>
  <Card title="Fraud and scam signals" icon="triangle-exclamation">
    Flags phishing attempts, fake job and work-from-home offers, and requests for personal or financial information through a distinct `fraud` category, kept separate from generic spam scoring.
  </Card>
  <Card title="Server-side behavioral checks" icon="clock">
    When you pass form metadata, honeypot trips and submission timing fold into the same score, so obvious bot fills get caught without relying on client-side JavaScript.
  </Card>
</CardGroup>

### Response fields

<ResponseField name="score" type="float">
  Calibrated risk from `0.0` (clean) to `1.0` (almost certainly spam, fraud, or junk). The fusion of every signal sent.
</ResponseField>

<ResponseField name="decision" type="string">
  `allow`, `review`, or `block`, derived from `score` against your project thresholds. Branch your form handler on this field.
</ResponseField>

<ResponseField name="categories" type="object">
  Per-category probabilities: `spam`, `fraud`, and `cold_email`, each `0.0`–`1.0`. Lets you act differently on each kind of junk.
</ResponseField>

<ResponseField name="signals.content" type="object">
  The content model's breakdown: `risk`, `spam_probability`, detected `language`, and a `flags` array of human-readable tells (`promotional_language`, `urgency_markers`, `suspicious_links`).
</ResponseField>

## Common questions

### How do I stop form spam without adding a CAPTCHA?

POST each submission to `/v1/check` from your form handler and branch on the returned decision: `allow` saves it, `block` drops it, `review` queues it. The content model scores the text server-side, so there is no CAPTCHA challenge or client-side widget for legitimate users to clear.

### What is the difference between POST /v1/check and POST /v1/content?

`/v1/check` takes the full submission — form fields plus optional IP, email, and user agent — and fuses content, IP, and email signals into one decision. `/v1/content` scores a single block of text on its own when that is all you have. Both return the same `0.0`–`1.0` score and category probabilities.

### How many credits does a check cost?

Each check costs 2 credits. The decision and per-category probabilities (`spam`, `fraud`, `cold_email`) come back in the same response, so a single 2-credit call covers all three classifications.

## Next steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="code" href="/api-reference/introduction">
    The full `/v1/check` request shape, response envelope, and error codes.
  </Card>
  <Card title="Server reporting" icon="server" href="/guides/server-reporting">
    Capture crawlers and AI bots that never run JavaScript.
  </Card>
</CardGroup>
