api reference · v1

POST /v1/feedback

Updated September 21, 2026

Tell us a specific piece of text was spam or was not, once a human has settled it. The correction takes effect on your next prediction for that same text, is scoped to your own account, and costs nothing against your prediction quota. It is also the only thing that can push a score above max_confidence, which makes it the only route to a band you can safely act on automatically.

Request

Authenticate with the X-API-Key header. The body is a single JSON object:

fieldtypenotes
textstring1–20,000 characters. The correction is matched on the text itself, not on a request id, so send the same string you sent to /v1/predict. Matching is forgiving about case, Unicode compatibility forms and runs of whitespace, and about nothing else — truncating to a different length in the two calls is the usual reason a report appears to have no effect.
label"ham" | "spam"spam raises the score for that text; ham lowers it. Anything else is a 422.
curl
curl -sX POST https://api.siftfy.io/v1/feedback \
  -H "X-API-Key: $SIFTFY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Win a free iPhone! Click here NOW", "label": "spam"}'
python
import os
import requests

# Call this from wherever a human settles the question: a moderator
# approving or hiding a comment, a user pressing "Report spam" or
# "Not spam", an agent closing a ticket as junk.
def report(text: str, label: str) -> None:
    resp = requests.post(
        "https://api.siftfy.io/v1/feedback",
        headers={"X-API-Key": os.environ["SIFTFY_KEY"]},
        json={"text": text, "label": label},   # "ham" or "spam"
        timeout=5,
    )
    resp.raise_for_status()   # 202 on accept
javascript
await fetch("https://api.siftfy.io/v1/feedback", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": process.env.SIFTFY_KEY,
  },
  body: JSON.stringify({ text, label: "spam" }),
});

Response

202 Accepted:

json
{
  "status": "accepted",
  "label": "spam",
  "report_count": 1,
  "model_version": "bert-tiny-spam-signals+cal3"
}
fieldtypenotes
statusstringaccepted.
labelstringThe label we recorded, echoed back.
report_countnumberHow many times your account has reported this exact text with this label. Bookkeeping, not signal strength — see below.
model_versionstringThe model serving when the correction was recorded. Empty if the online store was briefly unreachable; the durable record is still written and the response is still a 202.

What it does to the next score

A correction is folded in after calibration and after the ceiling clamp, which is why it is the one thing that can exceed the ceiling. From a prior score p, one confirmed report moves the next prediction to:

labelnext scorefrom 0.50from 0.65from the ceiling
spamp + (1 − p) × 0.610.800.860.94
hamp × 0.390.200.250.33

Two consequences worth designing around. First, a spam report does not always clear the ceiling: it only does so when the model already read the text at roughly 0.59 or above. Report something scored 0.50 and the corrected score lands near 0.80, under the ceiling and indistinguishable from an uncorrected graded score. Second, the online effect is the same whether you report once or twenty times. Your account holds at most one label per text and the latest report wins, so report_count tells you about your own reporting history and nothing about how hard the score moved. Reporting in a loop does not make a correction stick harder.

Corrections are scoped to your account. Your reports change your predictions; they do not move anyone else's scores, and nobody else's reports move yours. Sending ham after spam for the same text replaces the label rather than cancelling out to nothing.

Quota and rate limits

Feedback has its own per-minute bucket at the same per-minute ceiling as your plan's prediction limit, and it does not consume your daily prediction quota — a correction is not a billable prediction. That is deliberate: the loop is the part of the system that makes automatic action possible, and we would rather you ran it on everything a human touches. See rate limits.

Errors

statuswhen
401Missing, invalid, or revoked X-API-Key, or an account that is not active.
422text missing or outside 1–20,000 characters, or label missing or not ham/spam. Field-level detail in the body, same shape as the other endpoints.
429Per-minute feedback rate limit exceeded.
503The durable record could not be written. Safe to retry, but not a no-op: the online store is written first, so the correction may already be affecting your next prediction. Retrying re-records the same label rather than doubling it.

Treat a failed correction as non-fatal in your own flow. A moderator's decision should land in your database whether or not we accepted the report; retry the report, do not roll back the decision.

What we keep

The scoring layer stores a salted one-way fingerprint of the text and the label — never the readable content. Matching works because both the report and the next prediction hash through the same function. The durable ledger records the fingerprint, the label, your account, the count and the timestamps, so a correction can be audited without the submitted text being retrievable. This is the practice described in privacy and terms. Because the readable text is not kept, a correction is not a corpus a model could be fitted on; what persists is the fingerprint, the label and the counts.

Where to next