All pages

Statements of reasons

The explanation the DSA says you owe the author of anything you remove, written with every block.

Under the EU's Digital Services Act (Regulation 2022/2065, article 17), a service that hosts what its users write and removes or restricts any of it owes the author a statement of reasons: what was done, why, whether it was automated, which of your rules it broke and how to contest it. It applies to every hosting service, whatever its size; the small-business exemption of article 19 does not cover it.

Switch statements on for a project and every block comes back with one, written from the verdict and your own rules, ready to show or send to the author. Nothing extra to call and nothing extra to pay.

Setting it up

  • The words, on the policy. In Policies, each rule has a Message: a title and a sentence saying what the rule asks of people ("Respect other people: do not insult..."). The statement quotes the message of the rule that refused the content. A rule with no message cites your terms in general.
  • The switch, on the project. In Settings (the project picked in the menu's selector), Statements: turn it on, say what you do with blocked content and where, and give your terms and where to appeal. Optionally for review too, when you hide held content until a person looks at it.

Every saved version of a policy keeps its messages, and every verdict records the version it was decided under. Each version's messages have a public page, /rules/{project}/{policy}/{version}, with an anchor per rule, and the statement links to the rule that acted, as it read that day.

In the answer

{
  "id": "mod_01jr7q...",
  "decision": "block",
  "flagged": ["harassment"],
  "statement": {
    "restrictions": ["removal"],
    "territories": [],
    "duration": null,
    "facts": {
      "flagged": ["harassment"],
      "reasons": ["Insults aimed at the reader"],
      "source": "own_initiative"
    },
    "automated": { "detection": true, "decision": true },
    "ground": {
      "type": "terms",
      "policy": { "slug": "comments", "version": 3 },
      "clauses": [
        { "key": "harassment", "title": "Respect other people",
          "body": "Do not insult, threaten, mock or target other people.",
          "url": "https://toxicfilter.com/rules/prj_01jr.../comments/3#harassment" }
      ],
      "terms_url": "https://example.com/terms"
    },
    "redress": { "internal": "appeals@example.com", "out_of_court": true, "judicial": true },
    "locale": "en",
    "text": "We have removed your content.\nWhy: it was identified as harassment.\n..."
  }
}
FieldArticle 17(3)What it says
restrictions, territories, duration(a)What was done, one measure or several, and in which countries (ISO codes; empty is everywhere). held for review content hidden until a person decides. duration is null: until further notice.
facts(b)What crossed a line and our reasons, never the content itself. source is own_initiative: the content was screened, not reported.
automated(c)Detection is always automated here. The decision is automated unless a person rejected it in the review queue.
ground(e)The messages of the policy rules that acted, from the policy version decided under, and your terms.
redress(f)Where to ask you for a review, plus out-of-court settlement and the courts.
text(4)All of it in plain words, in the language of the call's first locale: English, Spanish, Portuguese, French, Italian, German, Catalan or Dutch, and English for anything else.

Article 17(3)(d), the legal ground for content that is illegal, is not written: whether something breaks the law is your judgement, not ours. Every statement here rests on your own rules. Sending it to the author, when you have a way to reach them, is yours too.

Saying what you did

ToxicFilter never removes anything: your site does, and the statement has to say what it did. The project says what it usually does. A call can say otherwise for itself, with one measure or several:

{ "content": "...", "restriction": ["removal", "account_suspended"] }

The measures are removal, disabled, demoted, visibility and account_suspended. What the call said is kept with the verdict, so the statement rebuilt later states the same thing. It also goes on a batch item or the batch envelope.

GET /api/v1/records/{id}/statement

The same statement for a verdict already filed, rebuilt from the record and the rules version kept on it: the words that were sent, even after your rules changed. Add ?locale=es for another language. Free, like the rest of the review queue.

curl "https://toxicfilter.com/api/v1/records/mod_01jr7q.../statement?locale=es" \
  -H "Authorization: Bearer tf_live_..."

A held verdict a person rejected gets a statement with "automated": {"decision": false}. One a person approved, or one that was allowed, restricts nothing and answers 409 no_restriction.

Appeals (article 20)

Online platforms must let the author contest a restriction, free and electronically, for at least six months, and have a person decide it. Your site collects the complaint and files it here; it waits in the panel's review queue under Appeals.

curl -X POST "https://toxicfilter.com/api/v1/records/mod_01jr7q.../appeal" \
  -H "Authorization: Bearer tf_live_..." \
  -H "Content-Type: application/json" \
  -d '{"reason": "It was a recipe, not an insult."}'

The author's words are optional, kept encrypted, and removed a month after the appeal is decided. One appeal per verdict; 409 with appeal_filed, no_restriction or appeal_window_closed (six months after the decision) otherwise.

curl -X POST "https://toxicfilter.com/api/v1/records/mod_01jr7q.../appeal/resolve" \
  -H "Authorization: Bearer tf_live_..." \
  -H "Content-Type: application/json" \
  -d '{"outcome": "reversed", "moderator": "ana", "explanation": "A recipe after all.", "locale": "es"}'

outcome is upheld or reversed, and moderator and explanation are required: article 20 wants the decision taken by a person and explained. The answer carries appeal_decision, the reasoned decision ready to send to the complainant, and an appeal.resolved webhook tells your site, which is what puts reversed content back. After an upheld appeal the statement says a person decided; after a reversed one there is no restriction left and no statement.

The Transparency Database (article 24(5))

Online platforms that are not micro or small businesses also file every statement with the European Commission's DSA Transparency Database. Two ways:

  • Automatically. Paste the API token the Commission gave you in Settings, Transparency and switch filing on. Each statement is sent as it is written, with your verdict id as its puid, so a retry is never filed twice. The token is kept encrypted and never shown again. A filed verdict carries transparency.uuid in GET /api/v1/records/{id}.
  • Yourself. Download the last 30 days from the same screen, or ask the API for any period of up to 31 days, a hundred at a time:
curl "https://toxicfilter.com/api/v1/statements/transparency?since=2026-10-01&until=2026-10-31" \
  -H "Authorization: Bearer tf_live_..."

Each entry is already in the Commission's shape (decision_visibility, decision_ground, category, territorial_scope, automated_decision, puid...), ready for its batch endpoint. next is the after for the following page. For the territorial scope, write the project's territory as country codes (ES, PT); anything else, or nothing, files the whole EU and EEA.

This helps you give the statements, appeals and filings articles 17, 20 and 24(5) ask for. It is not legal advice, and other duties of the DSA (notices, trusted flaggers, transparency reports) are not covered by it.