Analytics Guide
The analytics services turn raw quiz activity into readable insights: how a quiz performed, which questions were too hard, who your strongest participants are, and how a live event went. This guide explains what each report tells you, how every number is calculated, and where to get it — written for people consuming the data, not building it.
Before You Start
Authentication
Every analytics endpoint requires you to identify yourself as the quiz owner, in one of two ways:
Authorization: Bearer <token>header (a logged-in user token), orx-api-key: <key>header, where the key has the Owner role.
You only ever see analytics for your own quizzes. Asking about someone else's quiz behaves the same as asking about a quiz that doesn't exist.
The golden rules — what counts and what doesn't
Every number in every report follows the same ground rules. Keep these in mind and the figures will always make sense:
- Only the latest attempt counts. If a participant retakes a quiz, their previous attempts are ignored everywhere. A participant is always represented by their most recent attempt — analytics never double-counts retakes.
- Most reports only count finished attempts. An attempt that was started but never submitted is excluded from score-based reports. The exceptions are the activity heatmap/calendar and event stats, which deliberately look at started attempts to measure engagement.
- Deleted quizzes disappear. Once a quiz is deleted, its data no longer appears in any report.
- All times are UTC and all durations are in milliseconds.
- Percentages are rounded to 2 decimal places (e.g.
72.46).
Quiz Reports
Quiz Summary
GET /api/v1/analytics/quizzes/{quiz_id}/summary
The one-glance health check for a single quiz. Use it for a dashboard card: "342 attempts, 87 people, average 74%."
| Field | What it means | How it's calculated |
|---|---|---|
total_attempts | How many finished attempts the quiz has received | Count of submitted, latest attempts |
unique_participants | How many different people took the quiz | Each person counted once, no matter how many retakes |
avg_score | The typical raw score | Sum of everyone's score ÷ number of attempts |
avg_score_pct | The typical score as a percentage | average score ÷ quiz total points × 100 |
best_score / worst_score | The highest and lowest scores achieved | Simple maximum and minimum |
avg_time_taken_ms | How long people typically took | Average of each attempt's duration |
total_points | The maximum possible score | Taken from the quiz itself |
completion_rate | Share of counted attempts that were submitted | Submitted attempts ÷ counted attempts × 100 |
Because this report only looks at finished attempts in the first place, total_attempts here means submitted attempts. To see how many people started but never finished, use the trends series (below), which tracks completion rate over time.
Quiz Performance
GET /api/v1/analytics/quizzes/{quiz_id}/performance
The deep-dive on scores: how they spread out, whether people pass, and how the quiz trends over time. This is the endpoint behind a "performance" tab with a histogram and a line chart.
Optional query parameters:
| Parameter | Default | Meaning |
|---|---|---|
pass_threshold | 50 | The percentage score that counts as a "pass" (0–100) |
bucket_count | 10 | How many bars in the score histogram (2–50) |
granularity | daily | Trend resolution: hourly, daily, weekly, or monthly |
from / to | none | Limit the trend series to a date range |
What you get back:
-
mean— the average score. Add up every score, divide by the number of attempts: -
median— the middle score. Line everyone up from lowest to highest; the median is the person in the middle (with an even count, the midpoint between the two middle scores). Less distorted by a few extreme scores than the mean: if the mean is well below the median, a few very low scores are dragging the average down. -
std_deviation— how spread out the scores are (sample standard deviation):A small number means everyone scored close together; a large number means results varied a lot.
-
pass_rate— the percentage of attempts that scored at or above the pass threshold. Each attempt's score is first converted to a percentage: -
distribution— the histogram. Every attempt's percentage score is dropped into equal-width buckets between 0 and 100, where each bucket spans percentage points. With the default 10 buckets you get 0–10%, 10–20%, … 90–100%, each with a count of how many attempts landed there. -
trends— a time series. For each time bucket (hour/day/week/month) you get the attempt count, unique participants, average score, average score percentage, and completion rate for that period. Completion rate here is meaningful: it's the share of attempts started in that period that were actually submitted.
Top Quizzes
GET /api/v1/analytics/trends/top-quizzes
A leaderboard of your own quizzes. Answers "which of my quizzes gets the most traffic?" or "which one do people struggle with?"
| Parameter | Default | Meaning |
|---|---|---|
sort_by | attempts | Rank by attempts, participants, avg_score, or completion_rate |
sort_order | asc | asc or desc (use desc for a "top 10") |
limit | 10 | How many quizzes to return (1–50) |
Each row carries the same headline numbers as the quiz summary (attempts, unique participants, average score and percentage, completion rate, average time). Unlike the summary, this list includes quizzes with zero attempts, and its completion rate counts unfinished attempts too — so it's the honest "how many starters actually finish" number.
Question Reports
Question Stats
GET /api/v1/analytics/questions/{question_id}/stats— one question
GET /api/v1/analytics/quizzes/{quiz_id}/questions— every question in a quiz
These reports tell you whether your questions are doing their job. Two numbers matter most:
Difficulty index
difficulty_index (0 to 1) — the fraction of people who answered correctly. Despite the name, higher means easier: 0.9 means 90% got it right (easy), 0.2 means only 20% did (hard). A healthy quiz mixes values; a question near 0 or 1 tells you almost nothing about who knows the material.
Only the latest answer from each finished attempt is counted, and partially-correct answers count as wrong — an answer must be fully correct to enter the numerator.
Worked example: 40 people answered the question; 28 got it right.
70% answered correctly — a moderately easy question.
Discrimination index
discrimination_index (−1 to 1) — does this question separate strong participants from weak ones? It's the Pearson correlation between the points a participant earned on this question and their score on the whole quiz:
where, for each participant of the who answered:
- = points they earned on this question
- = their total score on the quiz attempt
- , = the averages of those two values across all participants
In plain words: for every participant, compare "did they do well on this question?" against "did they do well on the quiz overall?" If the two move together — strong participants get it right, weak participants get it wrong — the correlation is positive. If they're unrelated, it's near zero. If they move opposite — your best people get it wrong — it's negative.
- Above ~0.3 — a good question: people who did well overall tended to get it right.
- Near 0 — the question doesn't distinguish anyone (maybe trivia, maybe ambiguous). Also returned as exactly
0when the correlation can't be computed — e.g. everyone got the question right (no variation to correlate). - Negative — a red flag: your best participants got it wrong. Often means a misleading wording or a mis-keyed answer.
Worked example (intuition): on a 4-person quiz, total scores are 90, 80, 40, 30. On question A the same people earned 5, 5, 0, 0 — the two strongest got it right and the two weakest got it wrong, giving : an excellent discriminating question. On question B they earned 0, 0, 5, 5 — the weakest participants got it right, giving : review that question.
Timing and answer breakdowns
-
avg_time_ms/median_time_ms— how long people spent on the question. The average is the sum of all times ÷ number of answers; the median is the middle value when times are sorted. If the median is far below the average, a few people got stuck for a very long time. -
distractors— for multiple choice, multiple select, and true/false questions: every answer option with how many people picked it (selection_count) and what share of attempts that represents:This shows whether wrong options are actually tempting anyone. An option nobody picks is dead weight. (For multiple-select questions, one attempt can pick several options, so the rates can sum past 100%.)
-
most_common_wrong— the five most frequently given wrong answers. For open-text questions this is where you spot recurring misconceptions or accepted-answer gaps.
The per-quiz version accepts sort_by (difficulty, discrimination, avg_time) and sort_order (asc/desc), so "sort by discrimination ascending" surfaces your problem questions first.
Participant Reports
Participant Report
GET /api/v1/analytics/participants/{participant_id}/report
One person's full story across all of your quizzes.
-
Headline totals — how many different quizzes they attempted, total attempts, average score, and best score.
-
percentile_rank(0–100) — how this person compares with everyone else. For each quiz they took, we compute what fraction of that quiz's participants scored strictly below them:The report then averages that percentile across every quiz they took. A value of 85 reads as: "on average, this person scores better than 85% of participants." (In a one-person quiz the percentile is 0 — there was nobody to beat.)
-
score_progression— every submitted attempt in chronological order with score, percentage, and date. Plot it to see whether they're improving. -
category_performance— accuracy grouped by question category: total questions answered, how many were correct, andThis is the "strong in History, weak in Math" view.
Optional filters: quiz_ids (comma-separated list) and from/to dates, which narrow the score progression.
The report's avg_score_pct compares the participant's average score against the largest quiz total among their quizzes. If they took quizzes with very different point totals, treat it as approximate — the per-attempt score_pct values inside score_progression are exact.
Quiz Leaderboard
GET /api/v1/analytics/quizzes/{quiz_id}/participants
Everyone who finished a specific quiz, ranked. This is your results table or leaderboard.
Each row: participant alias, score, percentage, rank, number of attempts they made, time taken, and when they submitted. Rank is by score, highest first, and ties share the same rank — two people tied at the top are both rank 1, and the next person is rank 3.
Paginated (page, per_page, default 20 per page, max 100) and sortable by score, rank, attempts, or time.
Participant × Quiz Detail
GET /api/v1/analytics/participants/{participant_id}/quizzes/{quiz_id}/report
The drill-down when you click one row in the leaderboard: one person's result on one quiz, answer by answer.
- Their score, percentage, rank, and percentile for that quiz (percentile 90 = beat 90% of participants on this quiz).
answers— every question with the answer they gave, whether it was correct, the points earned, time spent, andquiz_avg_score— the average points everyone earned on that question, so you can see where this person beat or trailed the crowd.
Event Reports
Events are scheduled runs of a quiz (a live session with registration). These reports focus on turnout as much as scores.
Event Stats
GET /api/v1/analytics/events/{event_id}/stats
The post-event wrap-up. The key funnel:
| Field | What it means | How it's calculated |
|---|---|---|
total_registered | How many people signed up | Distinct registered participants |
total_attempted | How many actually started | Count of attempts in the event |
participation_rate | Turnout | started ÷ registered × 100 |
total_submitted | How many finished | Attempts with a submission |
completion_rate | Follow-through | finished ÷ started × 100 |
So "200 registered, 150 started (75% participation), 140 finished (93.33% completion)" reads directly off this response. You also get the average score (raw and as a percentage), a 10-bucket score distribution (same histogram idea as quiz performance), and peak_participation_time — the single moment when the largest number of participants started their attempt.
Event Comparison
GET /api/v1/analytics/events/compare?event_ids=<id1>,<id2>,...
Side-by-side numbers for 2 to 10 events (comma-separated IDs), returned in chronological order: average score and percentage, completion rate, participation rate, attempted, and submitted. Ideal for "did the afternoon session outperform the morning one?"
Event Timeline
GET /api/v1/analytics/events/{event_id}/timeline
Activity during the event, minute by minute. Time is split into buckets (bucket_minutes, default 15, anywhere from 1 minute to a day) and each bucket reports:
new_attempts— how many people started in that windowsubmissions— how many finished in that window
Chart both lines together to see the wave of starts at the beginning and the wave of submissions near the deadline.
Platform Dashboard
These three endpoints cover all your quizzes at once — the data behind a home dashboard.
Trends Overview
GET /api/v1/analytics/trends/overview
Two things in one response:
totals— lifetime numbers across your whole account: total quizzes, total distinct participants, total attempts, average score (raw and percentage), and average completion rate. These totals include unfinished attempts, so the completion rate is the true "starters who finish" figure.series— the same time series as quiz performance trends (attempts, participants, average score, completion rate per period), but aggregated over all your quizzes. Acceptsgranularity(defaultdaily) andfrom/to.
Activity Heatmap & Calendar
GET /api/v1/analytics/trends/activity
When are people taking your quizzes?
heatmap— a grid of day-of-week × hour-of-day, each cell counting attempts started in that slot.day_of_weekruns 0 = Sunday through 6 = Saturday, andhouris 0–23 in UTC — shift to your local timezone before displaying.calendar— attempts started per calendar date, ready for a GitHub-style contribution calendar.
Both count started attempts (finished or not), because here you care about activity, not outcomes. Optional from/to narrow the window.
Reading the Numbers — Quick Reference
| Number | Range | Rule of thumb |
|---|---|---|
difficulty_index | 0–1 | Higher = easier. Aim for a mix; investigate anything near 0 or 1 |
discrimination_index | −1–1 | Above 0.3 good; near 0 uninformative; negative = review the question |
percentile / percentile_rank | 0–100 | "Beats X% of participants" |
completion_rate | 0–100% | Of those who started, how many finished |
participation_rate | 0–100% | Of those who registered, how many showed up |
pass_rate | 0–100% | Share of attempts at or above the pass threshold |
any *_pct | 0–100% | Score as a share of maximum points |
any *_ms | ≥ 0 | Milliseconds (divide by 1000 for seconds) |
Common gotchas
- A retake replaces the old attempt everywhere. Historical charts reflect the latest state, not what a report said last week.
- Ranks can tie. Ranks 1, 1, 3 is normal — don't assume they're consecutive.
- Empty ≠ error. A quiz with no submissions returns a "not found or has no submissions" error rather than a report full of zeros. Treat that case as "no data yet" in your UI.
- Timezones. Everything is UTC. The activity heatmap in particular will look shifted unless you convert to the viewer's timezone.