All pages

The response

Decision, scores, signals and what to do with each of them.

Every endpoint answers with the same object. Nothing is endpoint-specific.

{
  "id": "mod_01jr7q9x2c8h4m6v0b3n5k7t9d",
  "reference": "comment_9931",
  "decision": "review",
  "flagged": ["toxicity"],
  "scores": { "toxicity": 0.58, "profanity": 0.31 },
  "signals": [
    {
      "category": "toxicity",
      "score": 0.58,
      "detector": "ai_text",
      "reason": "Contempt aimed at the person being replied to.",
      "evidence": ["nobody asked, genius"]
    }
  ],
  "used_ai": true,
  "took_ms": 912,
  "cached": false,
  "project": "forum",
  "policy": { "slug": "listings", "version": 4 },
  "topics": { "gambling": 0.07 },
  "facts": { "language": "en", "language_confidence": 0.91 },
  "context": { "repeats": 0, "similar": 0 },
  "credits": { "remaining": 4771, "charged": 12, "renews_at": "2026-09-30T00:00:00+00:00" }
}

Fields

FieldType
idstringThis verdict's name. Keep it: it is what a false positive report, a support ticket and a webhook all refer to.
referencestring|nullWhatever you sent as reference, handed back. Your comment id, your listing id.
decisionstringallow, review or block. The one field you have to act on.
flaggedstring[]Everything that crossed its review line, worst first: categories by name, subjects as topic:gambling, lead types as lead:no_budget. A shortcut, not extra information.
scoresobjectCategory to confidence, 0 to 1. Only categories that scored appear.
signalsobject[]Why. One entry per detector that had something to say.
used_aibooleanWhether the model ran, or the cheap detectors settled it.
took_msintegerWall time inside the pipeline, not including the network.
topicsobjectHow much this is about each subject you measure, 0 to 1. A separate axis from scores. See topics. Absent when no topic is configured.
factsobjectWhat we noticed that is not a finding: the language, an address's canonical form, an image's fingerprint. Absent when there was nothing to notice.
contextobjectWhat we knew beyond the text: how often it has arrived, and the sender's record. See context. Absent when it was empty.
redactedstringThe content with the personal data masked, when you asked for it with redact.
degradedbooleanPresent and true only when you asked for the model and it could not be reached, so this verdict came from the cheap detectors alone. Different from used_ai: false, which means they settled it.
modelobjectPresent only when you asked for the model and we deliberately did not run it, which is a third thing again and not a failure. asked, read and why.
shadowobjectWhat a policy on trial would have decided. Recorded, never acted on. See policies.
projectstringThe project the verdict was filed under: the one the call named, or your default.
policyobjectWhich rules produced this verdict, and which version of them. default means the shipped ones, inline rules sent with the call, and overridden: true a policy with the call's rules laid over it. See policies.
cachedbooleanWhether this content had been judged before and the answer was remembered rather than recomputed.
creditsobjectremaining, charged for this call, and renews_at (ISO 8601). Cheap to watch on every call.

A review verdict also opens an entry in the queue, so the middle decision has somewhere to be decided rather than being a word in a response nobody acts on.

flagged

Everything that crossed its review line, worst first. A verdict is decided on three axes and this one field carries all of them, so a prefix is what says which axis a name belongs to and where its number is:

ShapeWhat it isIts number
toxicityA category: a claim that something is wrong with the content.scores
topic:gamblingA subject you gave a line to. Not a claim that anything is wrong, only that your rule is about it.topics
lead:no_budgetA kind of lead you gave a line to.leads

Categories keep their plain names, so code written against this field before the other two axes existed reads exactly what it always read. Only a measure with a line in your policy or in rules can appear here at all: a subject or a lead type nobody acted on is measured and reported, and named nowhere.

Neither of the other two axes produces a signal, because a measure is not a finding. So when one of them took the decision, this field is the only place that says what took it. Read it before telling somebody nothing was flagged about content you have just refused.

Signals

Field
categoryOne of the fifteen.
scoreThis detector's confidence, to three decimals. The score in scores is the aggregate.
detectorWhat found it. Text: term, policy_term, evasion, link_spam, scam, url, contact_details, sensitive_numbers, flood, repetition, script_abuse, language_mismatch, gibberish, prompt_injection. Across a conversation: pile_on, minor_safety. One person's words in a thread: account_rental, off_platform, rental_scam, job_scam, romance_scam. Only on their own surface: fake_review and discriminatory_job_ad. Identity: disposable_email, junk_name. Images: image_metadata, image_hash. Model: ai_text, ai_image. The measurements are not in here, because they are not findings: topic and lead answer in topics and leads, and image_provenance in facts.
reasonOne sentence a support agent can read out to the person whose content it is.
evidenceFragments copied verbatim from the content. Sometimes empty, when nothing specific carries the score. Card and phone numbers are masked here: a detector whose job is to notice a number that should not be public must not be the thing that copies it somewhere else.
spansWhere it was, as [offset, length] into the content you sent. Only from detectors that matched the raw text, because anything working on normalised text has no truthful way back and a mask on a guessed position covers the wrong words. This is what redact uses.

reason and evidence exist so you can tell somebody why their post was held without writing that sentence yourself. Show them. A rejection with no reason is what makes people think moderation is arbitrary.

Scores are ours, the decision is yours

decision is scores compared against our default thresholds. If your policy differs, ignore it and read scores directly: a dating app tolerates flirting and not harassment, a children's forum is the other way round, and a marketplace cares about spam above everything. That is also why this is fifteen categories and not one "toxicity" number, which would bake somebody else's policy into your product.

Claims about content, never about people

Every category describes the text or the image. "This text is harassment" can be checked; "this user is a harasser" is a claim about a human being, and no API has any business making it. Nothing here is a verdict on an account.

reference

Every endpoint accepts an optional reference: your own id for the thing being judged, up to 120 characters. It is never parsed, only stored and handed back, and it is what makes a verdict findable later by the name you already use for it. Send it. The alternative is keeping a map from our ids to yours, which is a table you have to write and keep correct.

cached

A verdict is a function of what went into it, so identical content submitted twice gets the same answer without running the pipeline again, which matters most on the calls that reach a model, where the repeat would otherwise pay for another reading. A cached call is billed as one check, one credit, whatever the original cost.

used_ai and cached answer different questions and both are worth reading. used_ai: true, cached: true means a model produced this verdict, earlier, for content identical to yours. Every call still gets its own id and its own row: it is a second question with the same answer, not the same question. For the same call made twice, see idempotency.

facts

Things worked out on the way past that are not claims that anything is wrong. Kept out of signals deliberately: a signal says something is wrong with the content, and putting this text is Spanish in the same list as this text is a threat makes every client filter it back out again.

language, language_confidenceText over 60 characters. Below that a guess is worse than nothing, because somebody will route on it.
email.canonical, email.domain, email.mxThe address as its provider reads it, and whether the domain can receive mail at all. a.n.a+tag@googlemail.com and ana@gmail.com are one inbox and two rows in your users table.
image.textWhat is written in the picture, transcribed. It has already been through the text detectors; see images.
age_signalSomebody in a conversation said how old they are. A fact and never a finding: a thirteen-year-old saying so is a child using a website, not a thing they did wrong.
image.hashA perceptual fingerprint, so you can recognise the same picture on your side too. It survives re-saving and re-compression, which a checksum of the file does not.

degraded

Model providers have outages. When ours is failing, a call that asked for the model gets the free detectors' verdict and "degraded": true, and is billed as the cheap call, because that is what ran.

It is a separate field rather than a quiet used_ai: false on purpose. Those are very different statements: one says the cheap detectors were enough, the other says nobody read it. Only the first is reassuring, and a client that treats an outage as a clean bill of health is exactly the failure this field exists to prevent. Hold or queue what matters to you when you see it.

After a few failures in a row the model is skipped outright for a minute rather than waited for on every request. One outage should not turn a service that answers in milliseconds into one that answers in seconds.

model

Present only when you asked for the model and we deliberately did not run it. Nothing failed and nothing is wrong: it is a decision of ours, and it is named rather than left for you to work out from the bill.

"model": { "asked": true, "read": false, "why": "conversation_sampling" }
why
conversation_samplingA message of a conversation with nothing in it for the free detectors, arriving between the points where the whole thread is read again. Every new message is a new call, so reading all of them would make a forty message thread forty model calls.

Three statements that look alike and are not, and this field is what separates the third from the other two. used_ai: false with no model means the cheap detectors settled it. degraded means nobody could read it. model means we chose not to. The verdict is the free detectors' verdict in all three cases, and the call is billed as the cheap one.