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:
| Decision | What it means | What to do |
|---|---|---|
allow | Nothing crossed a line. | Publish it. |
review | Something is off, not enough to be sure. | Publish and queue it, or hold it. Your policy. |
block | Confident 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 inreview.
Next
- Authentication, and why test keys exist.
- The response, field by field.
- Categories, and the threshold for each.