Classify migration guide

# Migrate from OOPSpam to Botpoison Classify

Three edits: swap the API key, point the same JSON request at Classify, and branch on Classify's label rather than on a threshold you chose for a score from 0 to 6. Your handler keeps its routes, its fields and its inbox.

Last verified 16 October 2026

## Why sites move off OOPSpam

OOPSpam returns a `Score` from 0 to 6, and the threshold is yours to pick; its docs treat 3 and above as spam. Classify's label carries a threshold that is published with its rate. Block what Classify labels spam, and 99.9% of real messages still get through. Measured across every site in our reference set. On a single site, expect a few more. The full table is on [labels and scores](https://botpoison.com/documentation/classify/labels-and-scores/).

OOPSpam reads the fields you pick out of the form: `content`, `senderIP`, `email`. Classify can read every field the form posted, so a pitch in the company field counts too.

## What maps to what

|  | OOPSpam | Classify |
| --- | --- | --- |
| Key | An `X-Api-Key` header | A secret key, as a bearer token, an `X-Api-Key` header or `secretKey` |
| Endpoint | POST to `/v1/spamdetection` on api.oopspam.com | POST to `/classify` on api.botpoison.com |
| What you send | `content`, `senderIP`, `email` and switches such as `blockTempEmail` | The message and email as `content`, with the same `X-Api-Key` header. `senderIP` has no counterpart |
| Answer | `Score` from 0 to 6, and `Details` on the checks that ran | `label`, spam or clean, a `score` from 0 to 1, and the `model` |
| Threshold | Yours to pick, 3 in the docs | Spam from 0.8, carried by the label |

## The three edits. Off OOPSpam, onto Classify

### Swap the key

Replace the OOPSpam API key with a Classify secret key, kept in your server's environment like the old one.

Shell

```diff
-OOPSPAM_KEY=xxxxxxxx
+BOTPOISON_CLASSIFY_SECRET_KEY=sk_xxxxxxxx
```

### Point the request at Classify

Keep the JSON request and the X-Api-Key header. Swap the URL and the key, send the message and email as content, name the model, and drop senderIP.

Node.js:

JavaScript

```diff
-const oopspam = await fetch("https://api.oopspam.com/v1/spamdetection", {
+const classify = await fetch("https://api.botpoison.com/classify", {
   method: "POST",
   headers: {
     "Content-Type": "application/json",
-    "X-Api-Key": process.env.OOPSPAM_KEY,
+    "X-Api-Key": process.env.BOTPOISON_CLASSIFY_SECRET_KEY,
   },
   body: JSON.stringify({
-    content: req.body.message,
-    senderIP: req.ip,
-    email: req.body.email,
+    content: { message: req.body.message, email: req.body.email },
+    model: "spam-standard",
   }),
 });
-const { Score } = await oopspam.json();
+const { label, score } = await classify.json();
```

PHP:

PHP

```diff
-$oopspam = json_decode(file_get_contents("https://api.oopspam.com/v1/spamdetection", false, stream_context_create(["http" => [
+$classify = json_decode(file_get_contents("https://api.botpoison.com/classify", false, stream_context_create(["http" => [
   "method" => "POST",
-  "header" => "Content-Type: application/json\r\nX-Api-Key: " . getenv("OOPSPAM_KEY"),
+  "header" => "Content-Type: application/json\r\nX-Api-Key: " . getenv("BOTPOISON_CLASSIFY_SECRET_KEY"),
   "content" => json_encode([
-    "content" => $_POST["message"],
-    "senderIP" => $_SERVER["REMOTE_ADDR"],
-    "email" => $_POST["email"],
+    "content" => ["message" => $_POST["message"], "email" => $_POST["email"]],
+    "model" => "spam-standard",
   ]),
 ]])));
```

Python:

Python

```diff
-oopspam = requests.post(
-    "https://api.oopspam.com/v1/spamdetection",
-    headers={"X-Api-Key": os.environ["OOPSPAM_KEY"]},
-    json={"content": request.form["message"], "senderIP": request.remote_addr, "email": request.form["email"]},
+classify = requests.post(
+    "https://api.botpoison.com/classify",
+    headers={"X-Api-Key": os.environ["BOTPOISON_CLASSIFY_SECRET_KEY"]},
+    json={"content": {"message": request.form["message"], "email": request.form["email"]}, "model": "spam-standard"},
 ).json()
```

Go:

Go

```diff
-ip, _, _ := net.SplitHostPort(r.RemoteAddr)
-body, _ := json.Marshal(map[string]string{
-  "content":  r.FormValue("message"),
-  "senderIP": ip,
-  "email":    r.FormValue("email"),
-})
-req, _ := http.NewRequest("POST", "https://api.oopspam.com/v1/spamdetection", bytes.NewReader(body))
+body, _ := json.Marshal(map[string]any{
+  "content": map[string]string{"message": r.FormValue("message"), "email": r.FormValue("email")},
+  "model":   "spam-standard",
+})
+req, _ := http.NewRequest("POST", "https://api.botpoison.com/classify", bytes.NewReader(body))
 req.Header.Set("Content-Type", "application/json")
-req.Header.Set("X-Api-Key", os.Getenv("OOPSPAM_KEY"))
+req.Header.Set("X-Api-Key", os.Getenv("BOTPOISON_CLASSIFY_SECRET_KEY"))
 res, err := http.DefaultClient.Do(req)
 if err != nil {
   http.Error(w, "", http.StatusBadGateway)
   return
 }
```

HTTP:

Shell

```diff
-curl "https://api.oopspam.com/v1/spamdetection" \
-  --header "X-Api-Key: $OOPSPAM_KEY" \
+curl "https://api.botpoison.com/classify" \
+  --header "X-Api-Key: $BOTPOISON_CLASSIFY_SECRET_KEY" \
   --header "Content-Type: application/json" \
-  --data '{"content": "Hello, could you send me a quote?", "senderIP": "203.0.113.7", "email": "jo@example.com"}'
+  --data '{"content": {"message": "Hello, could you send me a quote?", "email": "jo@example.com"}, "model": "spam-standard"}'
```

### Branch on the label, tune with the score

The label carries the spam threshold, so there is no number to pick for it. The score is there for a review queue of your own.

Node.js:

JavaScript

```diff
-if (Score >= 3) return fileAway(req.body);
+if (label === "spam") return fileAway(req.body);
+if (score >= 0.65) return holdForReview(req.body);
```

PHP:

PHP

```diff
-if ($oopspam->Score >= 3) { file_away($_POST); exit; }
+if ($classify->label === "spam") { file_away($_POST); exit; }
+if ($classify->score >= 0.65) { hold_for_review($_POST); exit; }
```

Python:

Python

```diff
-if oopspam["Score"] >= 3:
+if classify["label"] == "spam":
     return file_away(request.form)
+if classify["score"] >= 0.65:
+    return hold_for_review(request.form)
```

Go:

Go

```diff
-var out struct{ Score float64 `json:"Score"` }
+var out struct {
+  Label string  `json:"label"`
+  Score float64 `json:"score"`
+}
 json.NewDecoder(res.Body).Decode(&out)
-if out.Score >= 3 {
+if out.Label == "spam" {
   fileAway(r.PostForm)
   return
 }
+if out.Score >= 0.65 {
+  holdForReview(r.PostForm)
+  return
+}
```

HTTP:

Shell

```diff
-# The answer: a Score from 0 to 6, spam from the threshold you pick
-{"Score": 4, ...}
+# The answer: the label carries the spam threshold, the score is for review
+{"label": "spam", "score": 0.8, "model": "spam-standard"}
```

## What behaves differently

One score. OOPSpam's `Details` says which check fired. Classify answers one score for the whole message.

An outage answers 503. It is not counted. Deliver the message as clean, so an outage never loses a real one.

## Questions

**What is the equivalent of a Score of 3?**

There is none: the two scales are built differently. Start with the label, and set a review queue from the score if you want one. The [labels and scores](https://botpoison.com/documentation/classify/labels-and-scores/) page lists the share of real messages at or above each point.

**Can I test it before I switch?**

Yes. With the secret key `test`, a message containing `botpoison-test-spam` answers spam and anything else answers clean. Test calls never count. See [test mode](https://botpoison.com/documentation/classify/test-mode/).

**Moving from Akismet instead?**

See the guide [from Akismet](https://botpoison.com/migrate/from-akismet/).

## Sources

- [OOPSpam API documentation](https://www.oopspam.com/docs/) OOPSpam

OOPSpam is a trademark of OOPSpam. Botpoison is not affiliated with OOPSpam.

## Other migration guides

### To Challenge

Invisible CAPTCHA alternative

- [From reCAPTCHA](https://botpoison.com/migrate/from-recaptcha/)
  v2 checkbox, v2 invisible or v3 score. Swap the script, the key and the siteverify call.
- [From Turnstile](https://botpoison.com/migrate/from-turnstile/)
  Drop the cf-turnstile widget. The check runs fresh on each Send and moves to one verify call.
- [From hCaptcha](https://botpoison.com/migrate/from-hcaptcha/)
  Remove the h-captcha div and the form-encoded siteverify request. Nobody labels images again.

### To Classify

Spam classifier

- [From Akismet](https://botpoison.com/migrate/from-akismet/)
  comment-check answers "true" or "false" as text. Classify answers a label, and a score to hold the borderline ones on.
