All pages

Quickstart

Authenticate, make your first call, and read the answer. Five minutes.

Every endpoint is a single POST with a JSON body, so there is nothing to configure and nothing you need in order to start: one call in, one verdict out. There are clients for PHP, Python and JavaScript when you want typed errors and retries, and they call exactly what is below.

Base URL

https://toxicfilter.com/api/v1

The version is in the path from the first day the API existed. Adding one later means either breaking every integration at once or running an unversioned endpoint forever beside a versioned one, and both are worse than a prefix nobody minds.

Your first call

Create a key in the dashboard, then:

curl https://toxicfilter.com/api/v1/text \
  -H "Authorization: Bearer tf_test_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Great post, thanks for writing this up.",
    "locales": ["en"],
    "surface": "comment"
  }'

And back:

{
  "id": "mod_01jr7q9x2c8h4m6v0b3n5k7t9d",
  "reference": null,
  "decision": "allow",
  "flagged": [],
  "scores": [],
  "signals": [],
  "used_ai": false,
  "took_ms": 1,
  "cached": false,
  "charged": 0,
  "policy": { "slug": "default", "version": 0 },
  "credits": { "remaining": 2000, "charged": 0, "renews_at": null }
}

Four fields in there are worth a sentence each, because they are the product.

used_ai: false. Ordinary content never reaches the model: the free detectors settle it, and that is nearly all traffic. You are billed for what actually ran, not for what might have.

took_ms: 1 is the time the checks took, rounded to the nearest millisecond. It grows with the length of the content and not much else, because what costs is reading it: a short comment lands at 1 ms, a paragraph at 3, a 2,500 character post at 4. It does not include the round trip to us, which is the part you will actually feel, so treat these as the floor rather than as a latency figure.

charged: 0 because that is a tf_test_ key. Test keys run every free check and cost nothing, which is what makes them worth using while you integrate; the model only reads calls made with a live key. The same call on a live key is 1 credit for a check the free detectors settle. When the model reads it, the tokens it used are added, rounded up to the next credit: about 8 in all for a comment, about 10 for an image.

renews_at: null on a brand new account, and only there. Your month starts with your first billed call rather than on a date we picked, so until then there is no date to give you and inventing one would be worse than the null.

Two more things worth doing on day one, both one line each. Send reference with your own id for the comment, so the verdict is findable later by the name you already use. And send an Idempotency-Key header, so a call that times out can be retried without being judged, or billed, twice.

policy: default means our thresholds decided this. When you make your own they are named here with their version, and the same pair is filed with the verdict, so a decision always says which rules judged it.

What to do with the answer

Read decision and act on it. It is three values, and the middle one is the one that makes this usable:

DecisionWhat it meansWhat to do
allowNothing crossed a line.Publish it.
reviewSomething is off, not enough to be sure.Publish and queue it, or hold it. Your policy.
blockConfident enough to refuse.Reject it, and tell the author why using signals[].reason.

If you only ever branch on block, you have installed a filter that is wrong in exactly the cases that matter. The interesting content is in review.

Next