Students get 3 attempts per case study by default. When a student runs out (due to technical issues, genuine failure, or special circumstances), faculty and admins can grant extra attempts — individually or in bulk — with optional expiry dates and full audit trails.
All mutations are append-only: every grant, revoke, and expiry is persisted as a transaction record, so the full history is always reconstructable.
List Student Attempts
List every student’s attempt entitlement for a given case study. One row per student, with usage, remaining count, best score, and grant status.
Authentication
Requires ATTEMPT_MANAGEMENT.can_view permission.
Query parameters
The case study to scope the list to.
Search by student name or email (case-insensitive).
Filter by entitlement state: has_remaining, exhausted, or has_extra.
sort_by
string
default:"student_name"
Sort field: student_name, attempts_used, attempts_remaining, best_score, or latest_attempt_at.
Number of records to skip.
Max records to return (1–100).
Example request
curl "https://staging-be.mind.miva.university/v1/console/attempts?case_study_id=6650c3d4e5f6a7b8c9d0e1f2&status=exhausted&sort_by=attempts_used&sort_order=desc" \
-H "Authorization: Bearer <access_token>"
Response
{
"success": true,
"data": [
{
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"student_name": "Jane Smith",
"student_email": "jane.smith@example.com",
"case_study_id": "6650c3d4e5f6a7b8c9d0e1f2",
"case_study_title": "Ethiopian Airlines Case Study",
"base_attempts": 3,
"extra_attempts": 2,
"revoked_attempts": 0,
"attempts_used": 4,
"total_allowed": 5,
"attempts_remaining": 1,
"best_score": 78.5,
"latest_attempt_at": "2026-04-01T14:30:00Z",
"has_active_grants": true
}
],
"total": 1,
"page": 1,
"page_size": 50,
"total_pages": 1,
"message": null
}
Field reference
| Field | Description |
|---|
base_attempts | Default allowance (usually 3) |
extra_attempts | Total granted beyond base, after expiries applied |
revoked_attempts | Total revoked by faculty/admins |
attempts_used | Real attempts the student has consumed (60s+ sessions) |
total_allowed | base + extra − revoked |
attempts_remaining | max(0, total_allowed − attempts_used) |
best_score | Highest graded score, or null if never graded |
has_active_grants | true if the student has any unexpired grant on this case study |
Notes
- Expired grants are processed lazily on read — the first request after expiry rolls back
extra_attempts and writes an EXPIRY transaction. No cron needed.
- Students with zero attempts used but with grants still appear in the list.
Get Student Detail
GET /v1/console/attempts/{user_id}
Full detail for a single student on a case study: entitlement summary, transaction history (grants, revokes, expiries), and per-session attempt list.
Authentication
Requires ATTEMPT_MANAGEMENT.can_view permission.
Path parameters
Query parameters
The case study to scope the detail to.
Example request
curl "https://staging-be.mind.miva.university/v1/console/attempts/6650a1b2c3d4e5f6a7b8c9d0?case_study_id=6650c3d4e5f6a7b8c9d0e1f2" \
-H "Authorization: Bearer <access_token>"
Response
{
"success": true,
"data": {
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"student_name": "Jane Smith",
"student_email": "jane.smith@example.com",
"case_study_id": "6650c3d4e5f6a7b8c9d0e1f2",
"case_study_title": "Ethiopian Airlines Case Study",
"entitlement": {
"base_attempts": 3,
"extra_attempts": 2,
"revoked_attempts": 0,
"attempts_used": 4,
"total_allowed": 5,
"attempts_remaining": 1
},
"transactions": [
{
"id": "6650f1a2b3c4d5e6f7a8b9c0",
"transaction_type": "grant",
"amount": 2,
"reason": "Student reported audio issues on 2nd attempt",
"actor_user_id": "6650b2c3d4e5f6a7b8c9d0e1",
"actor_name": "Dr. Mark Adebayo",
"expires_at": "2026-04-20T23:59:59Z",
"expired": false,
"created_at": "2026-04-02T10:15:00Z"
}
],
"attempts": [
{
"session_id": "6650d4e5f6a7b8c9d0e1f2a3",
"attempt_label": "Attempt 4",
"score": 78.5,
"status": "ended",
"started_at": "2026-04-01T14:20:00Z",
"ended_at": "2026-04-01T14:30:00Z",
"duration_seconds": 542.0,
"counted_as_attempt": true
}
]
},
"message": null
}
Transaction types
| Type | Meaning |
|---|
grant | Extra attempts added by a faculty/admin |
revoke | Attempts removed (cannot drop total below used) |
expiry | System-generated when a grant’s expires_at passes |
Grant Attempts (Single Student)
POST /v1/console/attempts/grant
Grant extra attempts to one student. Always requires an expiry date and reason. Supports idempotency.
Authentication
Requires ATTEMPT_MANAGEMENT.can_edit permission.
Request body
The case study to grant on.
Number of extra attempts (must be > 0).
Why the grant is being made (1–1000 chars). Shown in audit logs and the student email.
When the grant expires. Must be in the future (ISO 8601).
Client-generated unique key. Replaying the same key returns the current entitlement without re-applying.
Example request
curl -X POST "https://staging-be.mind.miva.university/v1/console/attempts/grant" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"case_study_id": "6650c3d4e5f6a7b8c9d0e1f2",
"amount": 2,
"reason": "Student reported audio issues on 2nd attempt",
"expires_at": "2026-04-20T23:59:59Z",
"idempotency_key": "grant-jane-eth-airlines-2026-04-02"
}'
Response
{
"success": true,
"data": {
"base_attempts": 3,
"extra_attempts": 2,
"revoked_attempts": 0,
"attempts_used": 4,
"total_allowed": 5,
"attempts_remaining": 1
},
"message": "Attempts granted successfully"
}
Side effects
- Writes a
grant transaction to the ledger
- Publishes
attempt.granted event → recorded in audit log
- Sends a branded email to the student with the new entitlement and expiry
Error responses
| Status | Condition |
|---|
400 | expires_at is in the past, amount <= 0, or reason missing |
404 | Student or case study not found |
Revoke Attempts (Single Student)
POST /v1/console/attempts/revoke
Revoke attempts from a student. Guarded: cannot revoke below the number of attempts already used.
Authentication
Requires ATTEMPT_MANAGEMENT.can_edit permission.
Request body
The case study to revoke on.
Number of attempts to revoke (must be > 0).
Why the revoke is being made (1–1000 chars).
Optional client-generated unique key.
Example request
curl -X POST "https://staging-be.mind.miva.university/v1/console/attempts/revoke" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"case_study_id": "6650c3d4e5f6a7b8c9d0e1f2",
"amount": 1,
"reason": "Accidental over-grant — correcting"
}'
Response
{
"success": true,
"data": {
"base_attempts": 3,
"extra_attempts": 2,
"revoked_attempts": 1,
"attempts_used": 4,
"total_allowed": 4,
"attempts_remaining": 0
},
"message": "Attempts revoked successfully"
}
Revoke guard
Revoke is rejected if it would reduce total_allowed below attempts_used. For example, if a student has used 4 of 5 allowed, the maximum revocable is 1. Attempting more returns a 400 with the exact available headroom.
Side effects
- Writes a
revoke transaction to the ledger
- Publishes
attempt.revoked event → recorded in audit log
- Sends a branded email to the student
Bulk Grant Attempts
POST /v1/console/attempts/grant/bulk
Grant extra attempts to up to 500 students at once. Runs asynchronously via Celery with per-row result tracking. Supports dry-run and idempotency.
Authentication
Requires ATTEMPT_MANAGEMENT.can_edit permission.
Request body
The case study to grant on.
List of student user IDs (1–500).
Number of extra attempts to grant each student.
Shared reason applied to all rows (1–1000 chars).
Expiry applied to all grants.
If true, validates all rows without writing transactions.
Optional. Replaying the same key returns the existing job.
Example request
curl -X POST "https://staging-be.mind.miva.university/v1/console/attempts/grant/bulk" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"case_study_id": "6650c3d4e5f6a7b8c9d0e1f2",
"user_ids": [
"6650a1b2c3d4e5f6a7b8c9d0",
"6650a2b3c4d5e6f7a8b9c0d1",
"6650a3b4c5d6e7f8a9b0c1d2"
],
"amount": 1,
"reason": "Service outage on 2026-04-01 — granting retry",
"expires_at": "2026-04-15T23:59:59Z",
"dry_run": false,
"idempotency_key": "bulk-outage-2026-04-01"
}'
Response
{
"success": true,
"data": {
"job_id": "6650f2a3b4c5d6e7f8a9b0c1",
"status": "queued",
"job_type": "grant",
"total_rows": 3,
"dry_run": false
},
"message": "Bulk grant job queued"
}
The job is queued immediately — poll Get Bulk Job Status to track progress and view per-row results.
Bulk Revoke Attempts
POST /v1/console/attempts/revoke/bulk
Revoke attempts from up to 500 students at once. Same contract as bulk grant, without the expires_at field. Each row is still guarded against revoking below used attempts — failing rows are reported in the job results but don’t abort the job.
Authentication
Requires ATTEMPT_MANAGEMENT.can_edit permission.
Request body
The case study to revoke on.
List of student user IDs (1–500).
Number of attempts to revoke from each student.
Shared reason (1–1000 chars).
If true, validates without writing.
Optional replay protection.
Example request
curl -X POST "https://staging-be.mind.miva.university/v1/console/attempts/revoke/bulk" \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"case_study_id": "6650c3d4e5f6a7b8c9d0e1f2",
"user_ids": ["6650a1b2c3d4e5f6a7b8c9d0", "6650a2b3c4d5e6f7a8b9c0d1"],
"amount": 1,
"reason": "Correction after accidental mass-grant",
"dry_run": true
}'
Response
{
"success": true,
"data": {
"job_id": "6650f3b4c5d6e7f8a9b0c1d2",
"status": "queued",
"job_type": "revoke",
"total_rows": 2,
"dry_run": true
},
"message": "Bulk revoke job queued"
}
Get Bulk Job Status
GET /v1/console/attempts/jobs/{job_id}
Poll the status of a queued or completed bulk job. Includes per-row results (success + error detail) once processing finishes.
Authentication
Requires ATTEMPT_MANAGEMENT.can_view permission.
Path parameters
The bulk job ID returned when the job was queued.
Example request
curl "https://staging-be.mind.miva.university/v1/console/attempts/jobs/6650f2a3b4c5d6e7f8a9b0c1" \
-H "Authorization: Bearer <access_token>"
Response
{
"success": true,
"data": {
"job_id": "6650f2a3b4c5d6e7f8a9b0c1",
"job_type": "grant",
"content_id": "6650c3d4e5f6a7b8c9d0e1f2",
"status": "completed",
"total_rows": 3,
"processed_rows": 3,
"succeeded_rows": 2,
"failed_rows": 1,
"results": [
{
"user_id": "6650a1b2c3d4e5f6a7b8c9d0",
"success": true,
"error": null
},
{
"user_id": "6650a2b3c4d5e6f7a8b9c0d1",
"success": true,
"error": null
},
{
"user_id": "6650a3b4c5d6e7f8a9b0c1d2",
"success": false,
"error": "Student not found"
}
],
"reason": "Service outage on 2026-04-01 — granting retry",
"amount": 1,
"expires_at": "2026-04-15T23:59:59Z",
"dry_run": false,
"started_at": "2026-04-02T09:00:01Z",
"completed_at": "2026-04-02T09:00:04Z",
"created_at": "2026-04-02T09:00:00Z"
},
"message": null
}
Job statuses
| Status | Meaning |
|---|
queued | In the queue, worker hasn’t picked it up yet |
processing | Worker running, rows being applied incrementally |
completed | All rows processed (check succeeded_rows / failed_rows for split) |
failed | Fatal error before row processing started |
Notes
- Bulk jobs use partial success: a failing row (invalid student, revoke guard hit) is recorded with its error and the job continues processing the rest.
- Results are updated row-by-row, so polling mid-flight shows incremental progress.
- Idempotency keys are honoured at the job level — replaying returns the same job.
Audit Trail
Every grant and revoke (single or bulk) publishes an event that the audit service persists:
| Event | When |
|---|
attempt.granted | Single grant succeeds |
attempt.revoked | Single revoke succeeds |
attempt.bulk_grant | Bulk grant job completes |
attempt.bulk_revoke | Bulk revoke job completes |
Query these via the Audit Events endpoint, filtered by event_type or actor_user_id.