Classify
Get a verdict for one form submission. The endpoint that does the work.
/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.
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 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
}' 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(); 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() $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
}
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
| code | when |
|---|---|
200 | Verdict returned. |
401 | Missing or invalid key. |
402 | Monthly quota exhausted. |
403 | Key revoked. |
413 | Body over 32 KB. |
422 | Validation error; detail lists the fields. |
429 | Over the per-minute rate limit; honour Retry-After. |
503 | Service 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
hameven 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_UNAVAILABLEandML_GRAY_FALLBACKand the classifier’s label is used. - Not idempotent. The same message sent twice returns
DUP_KNOWN_SPAMthe second time if the first verdict was confident spam. Do not retry a200. - 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.