diff --git a/api/api_specification.json b/api/api_specification.json
index 7955ce77c6d8d2bc14e48ef0aaaa40a916e4b0e0..4532455566dca552a901f488a28eb0e9e1ec5e7c 100644
--- a/api/api_specification.json
+++ b/api/api_specification.json
@@ -2299,6 +2299,128 @@
         }
       ],
       "url_parameters": null
+    },
+    {
+      "allow_while_disabled": false,
+      "allow_while_readonly": true,
+      "description": "",
+      "only_mod": true,
+      "request_objects": {},
+      "requires_csrf_token": false,
+      "response_description": "Note that views only appear some time after they viewer has watched the video (usually a few hours)",
+      "response_object_id": "",
+      "response_objects": {
+        "": {
+          "fields": {
+            "": {
+              "average_watch_speed": {
+                "config_directly_modifiable": false,
+                "id": "average_watch_speed",
+                "notes": "",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "float"
+              },
+              "daily_views": {
+                "config_directly_modifiable": false,
+                "id": "daily_views",
+                "notes": "",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "daily_views"
+              },
+              "lecture_views": {
+                "config_directly_modifiable": false,
+                "id": "lecture_views",
+                "notes": "",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "lecture_views"
+              },
+              "total_watched_seconds": {
+                "config_directly_modifiable": false,
+                "id": "total_watched_seconds",
+                "notes": "",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "int"
+              },
+              "view_count": {
+                "config_directly_modifiable": false,
+                "id": "view_count",
+                "notes": "",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "int"
+              }
+            }
+          },
+          "id": ""
+        },
+        "daily_views": {
+          "fields": {
+            "": {
+              "dates": {
+                "config_directly_modifiable": false,
+                "id": "dates",
+                "notes": "The dates in order without gaps",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "string[]"
+              },
+              "view_counts": {
+                "config_directly_modifiable": false,
+                "id": "view_counts",
+                "notes": "The count of views on the date which is at the same position in the 'dates' array",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "int[]"
+              }
+            }
+          },
+          "id": "daily_views"
+        },
+        "lecture_views": {
+          "fields": {
+            "": {
+              "lecture_ids": {
+                "config_directly_modifiable": false,
+                "id": "lecture_ids",
+                "notes": "The lecture ids (in ascending time order)",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "int[]"
+              },
+              "view_counts": {
+                "config_directly_modifiable": false,
+                "id": "view_counts",
+                "notes": "The count of views for the lecture on the date which is at the same position in the 'dates' array",
+                "object_variant": null,
+                "only_mod": false,
+                "optional": false,
+                "type": "int[]"
+              }
+            }
+          },
+          "id": "lecture_views"
+        }
+      },
+      "routes": [
+        {
+          "display_path": "/stats/course/{course_id}",
+          "method": "GET",
+          "path": "/stats/course/<int:course_id>"
+        }
+      ],
+      "url_parameters": null
     }
   ],
   "global_objects": {
diff --git a/api/api_specification.md b/api/api_specification.md
index 9f64bd71a6b2005fae23123e783ea21715441334..87bc26b6f6780917a2eb63f02f4526d963bcedc5 100644
--- a/api/api_specification.md
+++ b/api/api_specification.md
@@ -1,4 +1,4 @@
-# Specification of the Web API for the Video-AG Website (v0.86).
+# Specification of the Web API for the Video-AG Website (v0.87).
 
 ## Introduction
 
@@ -1743,6 +1743,98 @@ daily_views:
 Note that views only appear some time after they viewer has watched the video (usually a few hours)
 
 
+---
+#### `GET /stats/course/{course_id}`
+
+This may be used while the site is [read-only](#site-status).
+
+This may only be used by a moderator.
+
+###### Response:
+
+<table>
+<thead>
+    <th>Field</th>
+    <th>Type</th>
+    <th>Notes</th>
+</thead>
+<tbody>
+    <tr>
+        <td>average_watch_speed</td>
+        <td>float</td>
+        <td></td>
+    </tr>
+    <tr>
+        <td>daily_views</td>
+        <td>daily_views</td>
+        <td></td>
+    </tr>
+    <tr>
+        <td>lecture_views</td>
+        <td>lecture_views</td>
+        <td></td>
+    </tr>
+    <tr>
+        <td>total_watched_seconds</td>
+        <td>int</td>
+        <td></td>
+    </tr>
+    <tr>
+        <td>view_count</td>
+        <td>int</td>
+        <td></td>
+    </tr>
+</tbody>
+</table>
+
+
+daily_views:
+<table>
+<thead>
+    <th>Field</th>
+    <th>Type</th>
+    <th>Notes</th>
+</thead>
+<tbody>
+    <tr>
+        <td>dates</td>
+        <td>string[]</td>
+        <td>The dates in order without gaps</td>
+    </tr>
+    <tr>
+        <td>view_counts</td>
+        <td>int[]</td>
+        <td>The count of views on the date which is at the same position in the 'dates' array</td>
+    </tr>
+</tbody>
+</table>
+
+
+lecture_views:
+<table>
+<thead>
+    <th>Field</th>
+    <th>Type</th>
+    <th>Notes</th>
+</thead>
+<tbody>
+    <tr>
+        <td>lecture_ids</td>
+        <td>int[]</td>
+        <td>The lecture ids (in ascending time order)</td>
+    </tr>
+    <tr>
+        <td>view_counts</td>
+        <td>int[]</td>
+        <td>The count of views for the lecture on the date which is at the same position in the 'dates' array</td>
+    </tr>
+</tbody>
+</table>
+
+
+Note that views only appear some time after they viewer has watched the video (usually a few hours)
+
+
 ---
 
 
@@ -3228,6 +3320,10 @@ Possible `error_code`:
 
 ## Changelog
 
+### v0.87
+
+* Added `GET /stats/course/{course_id}`
+
 ### v0.86
 
 * Updated `ffmpeg_filter_graph_node`
diff --git a/api/api_specification_template.md b/api/api_specification_template.md
index d60bcbb1b962e6b4f958237d2e992916e39b9bb1..dea99d0895679ce100d09f11aba7f3a54c30adc6 100644
--- a/api/api_specification_template.md
+++ b/api/api_specification_template.md
@@ -1,4 +1,4 @@
-# Specification of the Web API for the Video-AG Website (v0.86).
+# Specification of the Web API for the Video-AG Website (v0.87).
 
 ## Introduction
 
@@ -139,6 +139,10 @@ Possible `error_code`:
 
 ## Changelog
 
+### v0.87
+
+* Added `GET /stats/course/{course_id}`
+
 ### v0.86
 
 * Updated `ffmpeg_filter_graph_node`
diff --git a/api/src/api/routes/stats.py b/api/src/api/routes/stats.py
index 2fcf35b81e7234fbe7f938f81ba49ef5bf3ec312..c78be0a7512b9bf4c7d36b0391cc22f37155a84b 100755
--- a/api/src/api/routes/stats.py
+++ b/api/src/api/routes/stats.py
@@ -1,6 +1,6 @@
 import math
 import re
-from datetime import timedelta
+from datetime import timedelta, date
 
 import more_itertools
 from typing import Sequence
@@ -288,8 +288,119 @@ def api_route_get_lecture_stats(lecture_id: int):
         "total_watched_seconds": generic_lecture_stats.total_watched_seconds if generic_lecture_stats else 0,
         "average_watch_speed": generic_lecture_stats.average_watch_speed if generic_lecture_stats else 1,
         "daily_views": {
-            "dates": [daily_views_dates_json],
-            "view_counts": [daily_views_counts_json]
+            "dates": daily_views_dates_json,
+            "view_counts": daily_views_counts_json
+        }
+    }
+
+
+@api_route("/stats/course/<int:course_id>", "GET", allow_while_readonly=True,
+           response_description="Note that views only appear some time after they viewer has watched the video (usually"
+                                " a few hours)",
+           response_objects={
+               "": [
+                   ("view_count", "int"),
+                   ("total_watched_seconds", "int"),
+                   ("average_watch_speed", "float"),
+                   ("lecture_views", "lecture_views"),
+                   ("daily_views", "daily_views"),
+               ],
+               "lecture_views": [
+                   ("lecture_ids", "int[]", "The lecture ids (in ascending time order)"),
+                   ("view_counts", "int[]", "The count of views for the lecture on the date which is at the same position in the"
+                                            " 'dates' array")
+               ],
+               "daily_views": [
+                   ("dates", "string[]", "The dates in order without gaps"),
+                   ("view_counts", "int[]", "The count of views on the date which is at the same position in the"
+                                            " 'dates' array")
+               ]
+           })
+@api_moderator_route()
+def api_route_get_course_stats(course_id: int):
+    
+    def _trans(session: SessionDb) -> tuple[
+        Course,
+        Sequence[tuple[date, int]],
+        Sequence[tuple[int, int]],
+        tuple[int, int, float],
+    ]:
+        course = session.scalar(
+            Course.select({
+                AC_IS_MOD: True
+            }, [])
+            .where(Lecture.id == course_id)
+        )
+        if course is None:
+            raise ApiClientException(ERROR_UNKNOWN_OBJECT)
+        daily_stats = session.execute(
+            sql.select(LectureDailyWatchStats.date, sql.func.sum(LectureDailyWatchStats.view_count))
+            .join(Lecture, LectureDailyWatchStats.lecture_id == Lecture.id)
+            .where(Lecture.course_id == course_id)
+            .group_by(LectureDailyWatchStats.date)
+            .order_by(LectureDailyWatchStats.date.asc())
+        ).all()
+        lectures_view_count = session.execute(
+            sql.select(LectureWatchStats.view_count, LectureWatchStats.lecture_id)
+            .join(Lecture, LectureWatchStats.lecture_id == Lecture.id)
+            .where(Lecture.course_id == course_id)
+            .order_by(Lecture.time.asc(), Lecture.id.asc())
+        ).all()
+        generic_course_stats = session.execute(
+            sql.select(
+                sql.func.sum(LectureWatchStats.view_count),
+                sql.func.sum(LectureWatchStats.total_watched_seconds),
+                sql.case(
+                    (
+                        sql.func.sum(LectureWatchStats.view_count) > 0,
+                        sql.func.sum(LectureWatchStats.average_watch_speed * LectureWatchStats.view_count)
+                        / sql.func.sum(LectureWatchStats.view_count)
+                    ),
+                    else_=0
+                )
+            )
+            .join(Lecture, LectureWatchStats.lecture_id == Lecture.id)
+            .where(Lecture.course_id == course_id)
+        ).one()
+        session.expunge_all()
+        return course, daily_stats, lectures_view_count, generic_course_stats
+    
+    course, daily_stats, lectures_view_count, generic_course_stats = database.execute_read_transaction(_trans)
+    
+    daily_views_counts_json = []
+    daily_views_dates_json = []
+    if len(daily_stats) > 0:
+        current_date = daily_stats[0][0]
+        daily_stat_iter = more_itertools.peekable(daily_stats)
+        while daily_stat_iter.peek(None) is not None:  # Has next
+            assert daily_stat_iter.peek()[0] >= current_date
+            
+            if daily_stat_iter.peek()[0] == current_date:
+                view_count = next(daily_stat_iter)[1]
+            else:
+                view_count = 0
+            daily_views_counts_json.append(view_count)
+            daily_views_dates_json.append(current_date.strftime("%d.%m.%Y"))
+            
+            current_date += timedelta(days=1)
+    
+    lecture_views_ids_json = []
+    lecture_views_counts_json = []
+    for row in lectures_view_count:
+        lecture_views_ids_json.append(row[1])
+        lecture_views_counts_json.append(row[0])
+    
+    return {
+        "view_count": int(generic_course_stats[0]),
+        "total_watched_seconds": int(generic_course_stats[1]),
+        "average_watch_speed": float(generic_course_stats[2]),
+        "lecture_views": {
+            "lecture_ids": lecture_views_ids_json,
+            "view_counts": lecture_views_counts_json
+        },
+        "daily_views": {
+            "dates": daily_views_dates_json,
+            "view_counts": daily_views_counts_json
         }
     }
     
diff --git a/common_py/src/videoag_common/objects/stats.py b/common_py/src/videoag_common/objects/stats.py
index b38d16cf8f2f6489c8a2ba193d7d2ba55288591b..90a53380da97a346f26ee4398094c1b5c6ffbf9a 100755
--- a/common_py/src/videoag_common/objects/stats.py
+++ b/common_py/src/videoag_common/objects/stats.py
@@ -76,6 +76,7 @@ class PublishMediumWatchStats(Base):
 # Counts the views per day for a lecture
 class LectureDailyWatchStats(Base):
     lecture_id: Mapped[int] = mapped_column(ForeignKey("lecture.id"), nullable=False, primary_key=True)
+    # TODO remove timezone
     date: Mapped[Date] = mapped_column(nullable=False, primary_key=True)
     
     view_count: Mapped[int] = mapped_column(nullable=False, default=0)