The review queue
Where `review` verdicts wait for a person, and how to tell us when one was wrong.
review means the system is not sure and a person should decide. This is
where those verdicts wait, and how you tell us afterwards whether we got it right.
Every endpoint here is free and outside the quota. Reading back a verdict you already paid for should not cost again, and charging you to tell us we were wrong would be charging you to improve our product.
GET /api/v1/records
curl "https://toxicfilter.com/api/v1/records?state=open&limit=50" \
-H "Authorization: Bearer tf_live_..."
{
"records": [
{ "id": "mod_01jr7q...", "reference": "comment_9931", "decision": "review",
"flagged": ["toxicity"], "scores": { "toxicity": 0.55 },
"signals": [ { "category": "toxicity", "score": 0.55, "detector": "term",
"reason": "Contains 1 profanity, aimed at the reader..." } ],
"used_ai": false, "took_ms": 2, "cached": false, "charged": 1,
"policy": { "slug": "default", "version": 0 } }
],
"next_before": "mod_01jr7q..."
}
| Filter | |
|---|---|
state | open (default), approved, rejected, resolved, any. |
decision | allow, review or block. |
feedback | correct, false_positive, false_negative, or none for what nobody has judged yet. |
reference | Your own id. The fastest way to find one verdict. |
project | One project's queue, by its slug. Left out, every project's. |
actor | Everything one user has posted, which is the question a moderator asks the moment they find one bad thing. |
kind | text, email, name, image. |
from, to | Dates. |
limit | 1 to 200, default 50. |
before | The next_before from the last page. |
Newest first, because a queue is worked from the top. Paging is by cursor rather than offset: rows keep arriving while somebody works, and an offset into a growing list skips whatever was inserted above it.
Only
reviewopens a queue entry. A block is a decision already taken, and filling the queue with everything that was ever refused would bury the things that actually need a person. You can still resolve a blocked verdict by naming it directly, which is what an appeal is.
GET /api/v1/records/{id}
One verdict, in full, plus what has been done about it and the content if there is any kept. The listing deliberately leaves the content out: a queue screen fetches fifty rows and needs fifty headlines.
POST /api/v1/records/{id}/resolve
curl https://toxicfilter.com/api/v1/records/mod_01jr7q.../resolve \
-H "Authorization: Bearer tf_live_..." \
-H "Content-Type: application/json" \
-d '{ "action": "approved", "moderator": "ana@example.com", "note": "Enthusiasm, not abuse." }'
action is approved or rejected.
moderator is your name for whoever did it, because we do not know
your staff and have no business inventing identities for them.
Resolving fires a moderation.resolved
webhook. That is what makes the queue in our dashboard usable: somebody approves a held
comment there and your site is told to publish it. Without it, a decision made here
would never reach the place it was about.
POST /api/v1/records/{id}/feedback
curl https://toxicfilter.com/api/v1/records/mod_01jr7q.../feedback \
-H "Authorization: Bearer tf_live_..." \
-H "Content-Type: application/json" \
-d '{ "verdict": "false_positive", "note": "Swearing is not abuse on this forum." }'
| verdict | |
|---|---|
correct | We got it right. |
false_positive | We flagged something that was fine. |
false_negative | We let through something that was not. |
One per verdict. Send it and you can then filter the queue by it, which turns "the filter feels wrong lately" into a list you can read, and usually into a threshold that wants moving by 0.05.
There is normally nothing to read
We do not store what you send us. The queue holds the verdict, its reasons and your
reference; the content lives on your side, which is where it was already.
For most integrations that is exactly right: your moderators work in your own admin, and
these endpoints tell them what to work on.
If your moderators are going to work in our dashboard, they need something to read. Set keep held content on a policy to a number of hours and we will keep the text of anything held or refused under it, encrypted, with an expiry written down at the moment it is stored, and never for content that was allowed.
Turning that on means we are holding your users' worst posts for you. It is off by default, it is capped at a week, and the number should be the smallest one your moderators can work with. The expiry is enforced when the row is read as well as by the job that empties it, so "six hours" means six.
Keys, from code
GET /api/v1/keys lists them (prefixes, modes, last use, but never a secret:
since we hold a hash) and POST /api/v1/keys/{id}/revoke revokes one,
including the one making the request. Both are free and on this same limiter.
There is no create. A key that can mint keys makes a leak permanent: revoke the one that got out and whatever it quietly created is still working, and you have no way to know what that was. Until keys can be scoped, minting stays in the dashboard where a person does it. A smaller API, and a much smaller way to lose an account.