---
title: "Analytics dashboard data for a group"
method: GET
path: "/api/v1/groups/statistics/dashboard/{id}/"
tags: ["Groups"]
---

# Analytics dashboard data for a group

`GET /api/v1/groups/statistics/dashboard/{id}/`

## Request

- Base URL: `https://plagiarismcheck.org`
- URL: `https://plagiarismcheck.org/api/v1/groups/statistics/dashboard/{id}/`
- Auth: API key in header `X-API-TOKEN`

Return every figure the analytics dashboard renders, in one read-only response: submission totals,
above-threshold counts, medians, membership, a time series, a score distribution, paginated
breakdowns by course and by assignment, and a paginated list of the submissions themselves.

**Access.** Group **owners only**. A teacher or student of the same group receives `403`, as does a
member of any other group.

**What counts as a submission.** A text belonging to the group, not deleted, whose `created_at`
(the moment it was submitted, not the moment its check finished) falls inside `[date_from, date_to]`
in the response timezone, and which matches the `course` / `course_id` and `assignment` /
`assignment_id` filters.

**Scores.** Plagiarism comes from the text's single plagiarism report. AI comes from the most recent
*completed* AI check on that text, so a later failed recheck does not erase an earlier score and a
later successful one replaces it. A submission missing either score still counts in
`data.submissions.total` but is excluded from every calculation based on that score: the
above-threshold counts, the median, the average, the distribution, and the table flag columns.

**Thresholds.** `ai_threshold` and `plagiarism_threshold` are independent and inclusive (`>=`). They
never affect `data.submissions.total`, `data.members`, or `filters.options`.

**Courses and assignments.** A course or assignment name exists only for submissions that arrived
through an LMS integration, and only where that integration recorded the name in the shared name
tables. Submissions without a resolvable course name are counted everywhere else but do not appear in
`data.courses`, `data.assignments`, or `filters.options`, so the rows of those tables can sum to
**less** than `data.submissions.total`. Names are merged across integrations and terms, so one course
name is one row however many LMS courses carry it. `course_id` and `assignment_id` identify those
names (one id per distinct name), not an LMS course or assignment.

**Submissions list.** `data.submissions.items` lists the filtered submissions one per row, with the
submitting user, both scores and the course and assignment. The date range and the `course` /
`assignment` filters apply to it like to everything else; the `submissions_*` parameters then narrow,
sort and page **the list only** - no total, chart or table changes when they do. So
`data.submissions.pagination.total_count` can be lower than `data.submissions.total`.

**Pagination.** Three independent paginators - `courses`, `assignments`, `submissions` - each with its
own `<table>_page` / `<table>_per_page` parameters and its own `pagination` object inside the table.
Read `pagination.page_count` to know where to stop; a page past the end is empty, not an error.

**Timezone.** Always `UTC`. Range boundaries and time-series buckets are drawn in it, and it is echoed
in `filters.timezone`.

**Caching.** A successful response is cached server-side for **ten minutes**, keyed by the group and
every parameter below, and cannot be bypassed from the request. Figures can therefore trail reality by
up to ten minutes, which is most visible during a submission deadline. Changing any parameter — a page
number or a sort direction included — is a fresh read rather than a cached one.

## Path parameters

- `id` integer, required

## Query parameters

- `date_from` string, date
- `date_to` string, date
- `course` string, nullable
- `assignment` string, nullable
- `course_id` integer, nullable
- `assignment_id` integer, nullable
- `step` 'day' | 'week' | 'month'
- `ai_threshold` number, float
- `plagiarism_threshold` number, float
- `courses_sort` 'course_name' | 'submissions' | 'ai_flag' | 'plagiarism_flag'
- `courses_dir` 'asc' | 'desc'
- `courses_page` integer
- `courses_per_page` integer
- `assignments_sort` 'assignment_name' | 'course_name' | 'submissions' | 'ai_flag' | 'plagiarism_flag'
- `assignments_dir` 'asc' | 'desc'
- `assignments_page` integer
- `assignments_per_page` integer
- `submissions_sort` 'submitted_at' | 'user_name' | 'user_email' | 'plagiarism_percent' | 'ai_percent' | 'course_name' | 'assignment_name'
- `submissions_dir` 'asc' | 'desc'
- `submissions_page` integer
- `submissions_per_page` integer
- `submissions_course` string, nullable
- `submissions_assignment` string, nullable
- `submissions_user` string, nullable
- `submissions_user_id` integer, nullable
- `submissions_flagged` 'ai' | 'plagiarism' | 'any', nullable

## Response `200`

The request succeeded and returned the dashboard figures.

- DashboardStatistics: Every figure the analytics dashboard renders.
  - `success` boolean, required
  - `filters` DashboardFilters, required: The normalized values actually used, defaults included. A client that sent nothing still learns which range, step, timezone, sorting and pagination produced the numbers below.
    - `group_id` integer, required: The group these figures describe, echoed from the path.
    - `date_from` string, date, required: First day of the range, inclusive.
    - `date_to` string, date, required: Last day of the range, inclusive.
    - `timezone` string, required: Timezone the range and bucket boundaries were drawn in. Always `UTC`.
    - `course` string, nullable, required: The course filter after trimming, or `null` when every course is included.
    - `assignment` string, nullable, required: The assignment filter after trimming, or `null` when every assignment is included.
    - `course_id` integer, nullable, required: The course id filter, or `null` when none was sent.
    - `assignment_id` integer, nullable, required: The assignment id filter, or `null` when none was sent.
    - `step` 'day' | 'week' | 'month', required
    - `ai_threshold` number, float, required
    - `plagiarism_threshold` number, float, required
    - `sort` object, required
      - `courses` DashboardSort, required
        - `field` string, required: The column sorted on. For `courses` one of `course_name`, `submissions`, `ai_flag`, `plagiarism_flag`; for `assignments` also `assignment_name`; for `submissions` one of `submitted_at`, `user_name`, `user_email`, `plagiarism_percent`, `ai_percent`, `course_name`, `assignment_name`.
        - `direction` 'asc' | 'desc', required
      - `assignments` DashboardSort, required
        - `field` string, required: The column sorted on. For `courses` one of `course_name`, `submissions`, `ai_flag`, `plagiarism_flag`; for `assignments` also `assignment_name`; for `submissions` one of `submitted_at`, `user_name`, `user_email`, `plagiarism_percent`, `ai_percent`, `course_name`, `assignment_name`.
        - `direction` 'asc' | 'desc', required
      - `submissions` DashboardSort, required
        - `field` string, required: The column sorted on. For `courses` one of `course_name`, `submissions`, `ai_flag`, `plagiarism_flag`; for `assignments` also `assignment_name`; for `submissions` one of `submitted_at`, `user_name`, `user_email`, `plagiarism_percent`, `ai_percent`, `course_name`, `assignment_name`.
        - `direction` 'asc' | 'desc', required
    - `pagination` object, required
      - `courses` DashboardPageRequest, required
        - `page` integer, required
        - `per_page` integer, required
      - `assignments` DashboardPageRequest, required
        - `page` integer, required
        - `per_page` integer, required
      - `submissions` DashboardPageRequest, required
        - `page` integer, required
        - `per_page` integer, required
    - `submissions` DashboardSubmissionFilters, required: The submissions list's own filters, normalized. Each is `null` when off. They narrow `data.submissions.items` only.
      - `course` string, nullable, required: The `submissions_course` search, trimmed.
      - `assignment` string, nullable, required: The `submissions_assignment` search, trimmed.
      - `user` string, nullable, required: The `submissions_user` search, trimmed.
      - `user_id` integer, nullable, required: The `submissions_user_id` filter.
      - `flagged` 'ai' | 'plagiarism' | 'any', nullable, required: The `submissions_flagged` filter.
    - `options` object, required: Dropdown contents for the frontend. Narrowed by the group and the date range only: thresholds and the table sort and pagination parameters never affect them, and `courses` in particular ignores the current `course` / `course_id` selection so it can still be changed. Send an option's id back as `course_id` / `assignment_id`.
      - `courses` DashboardCourseOption[], required: Distinct courses in the group and range, alphabetical by name.
        - `course_id` integer, required: Id of the course name. Accepted back as `course_id`.
        - `course_name` string, required
      - `assignments` DashboardAssignmentOption[], required: Distinct assignment and course pairs in the group and range, narrowed to `course` / `course_id` when one is set, ordered by assignment name then course name.
        - `assignment_id` integer, required: Id of the assignment name. Accepted back as `assignment_id`.
        - `assignment_name` string, required
        - `course_id` integer, required
        - `course_name` string, required
  - `data` DashboardData, required
    - `submissions` object, required: How many submissions arrived, and how many of them carry each kind of score. `total` is the only figure no threshold touches and the one a submission with no score at all still counts towards; the two scored counts are the denominators every rate below divides by, published so a reader can tell a clean cohort from one the checker never finished. They are tracked separately because a submission can carry a plagiarism score and no AI score. `items` and `pagination` are the submissions themselves, one row each, narrowed further by the `submissions_*` parameters.
      - `total` integer, required
      - `ai_scored` integer, required: Of those, how many have a usable AI score.
      - `plagiarism_scored` integer, required: Of those, how many have a plagiarism score.
      - `items` DashboardSubmissionRow[], required: One page of the filtered submissions, sorted by `submissions_sort`. Unlike the course and assignment tables it keeps submissions with no course and no score, as rows with `null`s.
        - `user_id` integer, required: The user who submitted the text (its creator). Accepted back as `submissions_user_id`.
        - `user_name` string, required: The user's name, or the part of their email before `@` when they have none.
        - `user_email` string, required
        - `text_id` integer, required
        - `plagiarism_percent` number, float, nullable, required: Rounded to two decimals. `null` when the text has no plagiarism score.
        - `ai_percent` number, float, nullable, required: From the latest AI check that produced a score, rounded to two decimals. `null` when there is none.
        - `course_id` integer, nullable, required: As in the courses table. `null` for a submission that did not come through an LMS.
        - `course_name` string, nullable, required
        - `assignment_id` integer, nullable, required
        - `assignment_name` string, nullable, required
        - `submitted_at` integer, required: When the text was submitted, as a unix timestamp - like `created_at` elsewhere in this API.
      - `pagination` Pagination, required: Pagination metadata returned by list endpoints.
        - `last` integer: Last available page number.
        - `current` integer: Current page number.
        - `num_items_per_page` integer: Requested page size.
        - `first` integer: First available page number.
        - `page_count` integer: Total number of pages.
        - `total_count` integer: Total number of items across all pages.
        - `page_range` integer: Number of page links included in the current range.
        - `start_page` integer: First page number in the displayed range.
        - `end_page` integer: Last page number in the displayed range.
        - `pages_in_range` integer[]: The page numbers the pager displays. Returned by every paginated endpoint; it was missing from this schema until the dashboard endpoint's contract test compared a real response against it.
        - `first_page_in_range` integer: First page number available in the current range.
        - `lastPage_in_range` integer: Last page number available in the current range.
        - `current_item_count` integer: Number of items returned on the current page.
        - `first_item_number` integer: One-based index of the first item on the current page.
        - `last_item_number` integer: One-based index of the last item on the current page.
    - `ai_above_threshold` DashboardThresholdCount, required: How many submissions scored at or above the relevant threshold, and what share of the **scored** submissions that is - `submissions.ai_scored` or `submissions.plagiarism_scored`, never `submissions.total`. A submission the checker never scored is not a zero, it is an absence: counting it in the denominator would let a silent gap in checking deflate the rate and read as a cleaner institution.
      - `count` integer, required
      - `percent` number, float, nullable, required: Rounded to two decimals. `null` - not `0.0` - when nothing was scored, since "nothing was checked" and "nothing was flagged" are different answers and only the second is 0%.
    - `plagiarism_above_threshold` DashboardThresholdCount, required: How many submissions scored at or above the relevant threshold, and what share of the **scored** submissions that is - `submissions.ai_scored` or `submissions.plagiarism_scored`, never `submissions.total`. A submission the checker never scored is not a zero, it is an absence: counting it in the denominator would let a silent gap in checking deflate the rate and read as a cleaner institution.
      - `count` integer, required
      - `percent` number, float, nullable, required: Rounded to two decimals. `null` - not `0.0` - when nothing was scored, since "nothing was checked" and "nothing was flagged" are different answers and only the second is 0%.
    - `median` object, required: Median score across every submission carrying that score, whatever the score is. The threshold does not narrow this: a median of the flagged papers only could never fall below the threshold and would say nothing about a typical paper. An even-sized set takes the mean of the two middle values. `null` - not `0.0` - when nothing was scored, because a median of nothing has no defensible value.
      - `ai` number, float, nullable, required
      - `plagiarism` number, float, nullable, required
    - `members` object, required: Current membership of the group. No filter on this endpoint changes these three numbers - not the date range, not the course or assignment, not the thresholds.
      - `total` integer, required: All members, whatever their role.
      - `teachers` integer, required: Members with the teacher role plus members with the owner role.
      - `students` integer, required
    - `timeseries` DashboardTimeseriesBucket[], required: One entry per `step` bucket covering the whole range in order, empty buckets included. The first and last may be partial, and their `period_start` and `period_end` are clipped to the range.
      - `period_start` string, date, required: First day of the bucket, inclusive, clipped to the requested range.
      - `period_end` string, date, required: Last day of the bucket, inclusive, clipped to the requested range.
      - `submissions` integer, required: Every submission in this bucket, scored or not - the bucket's volume. This is what volume bars under the rate lines are drawn from, and why they matter: a rate spike in a week of nine submissions reads as a crisis until the volume is visible beside it.
      - `ai_avg` number, float, nullable, required: Mean AI score across the bucket's scored submissions, weighted per submission rather than per day, and not narrowed by the threshold. `null` when `ai_scored` is 0 - a gap the line can skip, where `0.0` would draw down to the axis and read as a period when everyone scored nothing.
      - `ai_flagged` integer, required: Of the scored submissions, how many are at or above `ai_threshold`.
      - `ai_scored` integer, required: Submissions in this bucket carrying an AI score.
      - `ai_percent` number, float, nullable, required: `ai_flagged / ai_scored * 100`, or `null` when nothing was scored.
      - `plagiarism_avg` number, float, nullable, required: As `ai_avg`, for plagiarism.
      - `plagiarism_flagged` integer, required
      - `plagiarism_scored` integer, required
      - `plagiarism_percent` number, float, nullable, required
    - `distribution` DashboardDistributionBucket[], required: Always exactly ten buckets, in order: [0,10), [10,20), ... [80,90), [90,100]. The last is closed, so a score of 100 lands there rather than in an eleventh bucket. Every scored submission lands in exactly one bucket whatever the thresholds - this is the shape of the whole cohort, and the counts do not move when a threshold does. A client showing where the line falls colours the buckets at or above it. Submissions with no score are absent rather than counted as zero.
      - `from` integer, required: Lower edge of the bucket, inclusive.
      - `to` integer, required: Upper edge of the bucket, exclusive - except on the last bucket, where 100 is included.
      - `ai` integer, required: Submissions whose AI score falls in this bucket. Every scored submission is counted whatever `ai_threshold` is set to.
      - `plagiarism` integer, required: As `ai`, for plagiarism.
    - `courses` object, required: Submissions grouped by course name, sorted and paginated. Submissions with no resolvable course name are absent, so the rows can sum to less than `submissions.total`.
      - `items` DashboardCourseRow[], required
        - `course_id` integer, required: Id of the course name - one id per distinct name, not an LMS course id. Accepted back as the `course_id` filter.
        - `course_name` string, required: Grouped by name, so one row covers every LMS course carrying this name across integrations and terms.
        - `submissions` integer, required: Filtered submissions in this course. Thresholds do not apply.
        - `ai_flag` number, float, nullable, required: `ai_flag_count / ai_scored * 100`, rounded to two decimals - a share of the papers actually checked, not of every submission in the row. `null` when `ai_scored` is 0, which is a row whose submissions were never checked rather than a row where nothing was flagged; clients render that as `n/a` and keep `submissions` visible beside it.
        - `ai_flag_count` integer, required: Submissions in this course at or above `ai_threshold`.
        - `ai_scored` integer, required: Submissions in this course carrying an AI score - the denominator of `ai_flag`. Published because the two rates in a row can have different denominators and would otherwise look comparable.
        - `plagiarism_flag` number, float, nullable, required
        - `plagiarism_flag_count` integer, required
        - `plagiarism_scored` integer, required
      - `pagination` Pagination, required: Pagination metadata returned by list endpoints.
        - `last` integer: Last available page number.
        - `current` integer: Current page number.
        - `num_items_per_page` integer: Requested page size.
        - `first` integer: First available page number.
        - `page_count` integer: Total number of pages.
        - `total_count` integer: Total number of items across all pages.
        - `page_range` integer: Number of page links included in the current range.
        - `start_page` integer: First page number in the displayed range.
        - `end_page` integer: Last page number in the displayed range.
        - `pages_in_range` integer[]: The page numbers the pager displays. Returned by every paginated endpoint; it was missing from this schema until the dashboard endpoint's contract test compared a real response against it.
        - `first_page_in_range` integer: First page number available in the current range.
        - `lastPage_in_range` integer: Last page number available in the current range.
        - `current_item_count` integer: Number of items returned on the current page.
        - `first_item_number` integer: One-based index of the first item on the current page.
        - `last_item_number` integer: One-based index of the last item on the current page.
    - `assignments` object, required: Submissions grouped by assignment name and course name, sorted and paginated.
      - `items` DashboardAssignmentRow[], required
        - `assignment_id` integer, required: Id of the assignment name. Accepted back as the `assignment_id` filter.
        - `assignment_name` string, required: Grouped together with `course_name`, so the same assignment name in two courses is two rows.
        - `course_id` integer, required: Id of the course name - one id per distinct name, not an LMS course id. Accepted back as the `course_id` filter.
        - `course_name` string, required: Grouped by name, so one row covers every LMS course carrying this name across integrations and terms.
        - `submissions` integer, required: Filtered submissions in this course. Thresholds do not apply.
        - `ai_flag` number, float, nullable, required: `ai_flag_count / ai_scored * 100`, rounded to two decimals - a share of the papers actually checked, not of every submission in the row. `null` when `ai_scored` is 0, which is a row whose submissions were never checked rather than a row where nothing was flagged; clients render that as `n/a` and keep `submissions` visible beside it.
        - `ai_flag_count` integer, required: Submissions in this course at or above `ai_threshold`.
        - `ai_scored` integer, required: Submissions in this course carrying an AI score - the denominator of `ai_flag`. Published because the two rates in a row can have different denominators and would otherwise look comparable.
        - `plagiarism_flag` number, float, nullable, required
        - `plagiarism_flag_count` integer, required
        - `plagiarism_scored` integer, required
      - `pagination` Pagination, required: Pagination metadata returned by list endpoints.
        - `last` integer: Last available page number.
        - `current` integer: Current page number.
        - `num_items_per_page` integer: Requested page size.
        - `first` integer: First available page number.
        - `page_count` integer: Total number of pages.
        - `total_count` integer: Total number of items across all pages.
        - `page_range` integer: Number of page links included in the current range.
        - `start_page` integer: First page number in the displayed range.
        - `end_page` integer: Last page number in the displayed range.
        - `pages_in_range` integer[]: The page numbers the pager displays. Returned by every paginated endpoint; it was missing from this schema until the dashboard endpoint's contract test compared a real response against it.
        - `first_page_in_range` integer: First page number available in the current range.
        - `lastPage_in_range` integer: Last page number available in the current range.
        - `current_item_count` integer: Number of items returned on the current page.
        - `first_item_number` integer: One-based index of the first item on the current page.
        - `last_item_number` integer: One-based index of the last item on the current page.

## Other responses

- `403`: Authentication or authorization failed because the token is missing, invalid, blocked, or not allowed to access the resource.
- `404`: The requested resource was not found.
- `422`: One or more query parameters were rejected. `errors` maps the parameter name to a message; every invalid parameter in the request is reported at once rather than one per response.

## Changes

- **2026-10-07** `741c7347ef02`: 1 breaking, 25 info
  - the `filters/options/courses/items/` response's property type changed from `string` to `object` for status `200`
  - added the new optional `query` request parameter `assignment_id`
  - added the new optional `query` request parameter `course_id`
  - added the new optional `query` request parameter `submissions_assignment`
  - …22 more
- **2026-09-21** `9736bbb77e84`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/plagiarismcheck/apis/plagiarismcheck-org-api/changes/api/v1/groups/statistics/dashboard/:id/get.md)

---

[API](https://skmtc.dev/plagiarismcheck/apis/plagiarismcheck-org-api.md) - [All operations](https://skmtc.dev/plagiarismcheck/apis/plagiarismcheck-org-api/llms.txt) - [OpenAPI document](https://skmtc.dev/plagiarismcheck/apis/plagiarismcheck-org-api/revisions/2c81c29267dc?raw)
