API reference/ Classify
API reference

Classify

Get a verdict for one form submission. The endpoint that does the work.

POST /v1/classify API key

Send the fields your form collected and the visitor’s IP. Receive a label, a score, the stage that decided and the reasons.

Request

Content-Type: application/json. The whole body must be 32 KB or less. Unknown fields are accepted and ignored, except the key’s configured honeypot field.

Body
message string required

The message body. Up to 20,000 characters. The only required field.

name string default ""

The visitor’s name as typed. Up to 200 characters.

email string default ""

The visitor’s email address. Up to 320 characters. Used for identity checks and sender reputation.

subject string default ""

Subject line, if the form has one. Up to 500 characters.

honeypot string

Value of your hidden trap field. Any non-empty value is a hard spam verdict. If the key’s honeypot_field is set to another name (say website), send the value under that name instead and the API reads it from there.

form_load_ts number

Unix time in seconds when the form was rendered.

submit_ts number

Unix time in seconds when the form was submitted. Under two seconds after form_load_ts is a hard spam verdict.

ip string

The visitor’s IP address as your server saw it. Never your server’s own address. Enables IP reputation.

user_agent string

The visitor’s User-Agent header, up to 512 characters. Accepted for forward compatibility; not stored and not used by the model today.

locale string

Overrides the key’s locale for this request, for example de. Latin locales treat Cyrillic or CJK text as a soft signal.

mode "full" | "fast" default "full"

full may wait up to three seconds on the language model for gray-zone cases. fast never waits: gray-zone cases return the classifier’s nearest label in a few milliseconds with LLM_SKIPPED and ML_GRAY_FALLBACK. See Fast mode.

curl
curl https://api.spamex.dev/v1/classify \
  -H "Authorization: Bearer $SPAMEX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mike",
    "email": "[email protected]",
    "message": "I noticed your site is not on page 1 of Google. We can fix that, reply for a free audit.",
    "ip": "203.0.113.9",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
    "honeypot": "",
    "form_load_ts": 1757000000,
    "submit_ts": 1757000041
  }'
Node
const r = await fetch("https://api.spamex.dev/v1/classify", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SPAMEX_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Mike",
    email: "[email protected]",
    message: "I noticed your site is not on page 1 of Google. We can fix that, reply for a free audit.",
    ip: "203.0.113.9",
    honeypot: "",
    form_load_ts: 1757000000,
    submit_ts: 1757000041,
  }),
  signal: AbortSignal.timeout(5000),
});
const verdict = await r.json();
Python
r = httpx.post(
    "https://api.spamex.dev/v1/classify",
    headers={"Authorization": f"Bearer {SPAMEX_KEY}"},
    json={
        "name": "Mike",
        "email": "[email protected]",
        "message": "I noticed your site is not on page 1 of Google. We can fix that, reply for a free audit.",
        "ip": "203.0.113.9",
        "honeypot": "",
        "form_load_ts": 1757000000,
        "submit_ts": 1757000041,
    },
    timeout=5.0,
)
verdict = r.json()
PHP
$ch = curl_init('https://api.spamex.dev/v1/classify');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SPAMEX_KEY'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'name'         => 'Mike',
        'email'        => '[email protected]',
        'message'      => 'I noticed your site is not on page 1 of Google. We can fix that, reply for a free audit.',
        'ip'           => '203.0.113.9',
        'honeypot'     => '',
        'form_load_ts' => 1757000000,
        'submit_ts'    => 1757000041,
    ]),
]);
$verdict = json_decode(curl_exec($ch), true);

Response

{
  "id": "3f0c1a9e2b7d4e1f9c3a5b6d7e8f9a0b",
  "label": "spam",
  "score": 0.97,
  "stage": "ml",
  "reasons": ["NAME_EMAIL_MISMATCH", "SPAM_PHRASE", "ML_SPAM"],
  "latency_ms": 1.4
}
Fields
id string

Verdict id. Keep it: feedback and deletion refer to it.

label "spam" | "ham"

The decision. Always one of the two.

score number

Probability of spam in [0, 1] after rules and model are blended. For a language-model verdict, the model’s stated confidence.

stage "rules" | "ml" | "llm"

Which stage decided: memory, reputation or a hard rule; the classifier; or the language model.

reasons string[]

Stable reason codes in evaluation order. Never empty.

latency_ms number

Server-side time spent on this request.

Status codes

codewhen
200Verdict returned.
401Missing or invalid key.
402Monthly quota exhausted.
403Key revoked.
413Body over 32 KB.
422Validation error; detail lists the fields.
429Over the per-minute rate limit; honour Retry-After.
503Service cannot classify right now; retry once, then deliver.

The errors page has the format and the recommended handling for each.

Behaviour worth knowing

  • Deterministic first. Memory and reputation lookups run before anything else. A message you marked legitimate through feedback comes back ham even if it contains three links.
  • The language model never blocks you. Gray-zone calls have a three-second timeout. On timeout or error the response carries LLM_UNAVAILABLE and ML_GRAY_FALLBACK and the classifier’s label is used.
  • Not idempotent. The same message sent twice returns DUP_KNOWN_SPAM the second time if the first verdict was confident spam. Do not retry a 200.
  • Nothing raw is stored by default. A hash and a fingerprint of the text, hashed sender identifiers, the verdict. Retention is opt-in per key. See Data and privacy.
↑↓ navigate↵ openDocs and blog