# Quiz Lifecycle Events and Webhooks

Nine typed project_todo.quiz.* events for assignment, reminders, submissions, grading, release, pass/fail and To-do completion, pushed by webhooks and served by GET /updates.

Source: https://www.opentrain.ai/docs/developers/api-reference/project-todos/quizzes/events/

Endpoint: GET /api/public/v1/updates

Every change to a quiz To-do produces one typed event. A [webhook subscription](https://www.opentrain.ai/docs/developers/api-reference/webhooks/create/) pushes it to your endpoint, and [`GET /updates`](https://www.opentrain.ai/docs/developers/api-reference/updates/poll/) serves the same record. Your agent can follow a quiz from assignment to completion without opening OpenTrain.

**Requirements:** `project_todos:read` to see these events. To subscribe a webhook you also need `webhooks:manage` and the webhooks feature.

## Event types

| `type` | When it happens | Extra `data` fields |
| --- | --- | --- |
| `project_todo.quiz.assigned` | A quiz To-do is assigned and bound to its published version. Once per assignment. | `completionPolicy` |
| `project_todo.quiz.reminder_due` | An open quiz To-do needs a reminder: its deadline is close, it is overdue, or a required To-do without a deadline is still unopened. Once per reminder step. | `reminderKind`, `dueAt`, `cadenceBucket` |
| `project_todo.quiz.attempt_submitted` | The AI trainer submits an attempt. Once per attempt. | `attemptId`, `attemptNumber`, `attemptKind`, `reviewCycleId`, `gradeState`, `resultReleased` |
| `project_todo.quiz.grading_required` | A submitted attempt has manually graded questions that wait for your grade. Once per attempt. | `attemptId`, `attemptNumber`, `pendingQuestionCount` |
| `project_todo.quiz.graded` | A grade is saved. `PARTIAL` leaves questions ungraded; `FINALIZED` grades all of them. Once per grade revision. | `attemptId`, `attemptNumber`, `revisionNumber`, `gradeState`, `outcome`, `afterRelease` |
| `project_todo.quiz.result_released` | The result becomes visible to the AI trainer, either automatically or through your release. Once per attempt. | `attemptId`, `attemptNumber`, `releaseMode`, `releasedAt` |
| `project_todo.quiz.passed` | The attempt passes. This fires at submit for an automatically graded quiz, or on the grade that first decides the result or changes it. | `attemptId`, `attemptNumber`, `revisionNumber`, `previousOutcome` |
| `project_todo.quiz.failed` | The attempt fails. Same timing as `passed`. | `attemptId`, `attemptNumber`, `revisionNumber`, `previousOutcome` |
| `project_todo.quiz.todo_completed` | The quiz To-do is completed: by the submission, by a passing grade, or by your approval. | `completedVia`, `attemptId` |

Every event's `data` also carries `assignmentId` (the same value as the event's `resourceId`), `bindingId`, `formId` and `formVersionId`. `jobId` on the event envelope is the job.

`passed` and `failed` only fire for quizzes with a pass rule. A new grade that keeps the same result sends `graded` but no second `passed` or `failed`. `revisionNumber` is `null` when the automatic grade at submit decided the result.

### Field values

| Field | Values |
| --- | --- |
| `completionPolicy` | `ON_SUBMIT`, `ON_REVIEW`, `ON_PASS` |
| `reminderKind` | `DUE_SOON`, `OVERDUE`, `UNVIEWED` (`dueAt` is `null` for `UNVIEWED`) |
| `attemptKind` | `FULL`, or `REMEDIATION` for a [selective coaching](https://www.opentrain.ai/docs/developers/api-reference/project-todos/quizzes/coaching/) revision (`reviewCycleId` is set) |
| `gradeState` on `attempt_submitted` | `GRADED`, `PENDING_MANUAL` |
| `gradeState` on `graded` | `PARTIAL`, `FINALIZED` |
| `outcome`, `previousOutcome` | `PASSED`, `FAILED`, or `null` (not decided, or no pass rule) |
| `releaseMode` | `IMMEDIATE`, `MANUAL` |
| `completedVia` | `SUBMISSION`, `PASS`, `EMPLOYER_APPROVAL` (`attemptId` is `null` for an approval) |

## What payloads never contain

Payloads hold IDs and states only. They never contain answers, answer keys, rubrics, points, grading notes, names or email addresses. To read the details of an attempt, call [`GET /quizzes/attempts/{attemptId}`](https://www.opentrain.ai/docs/developers/api-reference/project-todos/quizzes/grading-and-results/) with the `attemptId` from the event.

## No duplicates on retries

OpenTrain writes each event in the same database transaction as the change it describes, keyed on that change. So:

- A retried or concurrent submit, grade, release or approval produces exactly one event per change.
- An event is never lost while its change is saved. If the event cannot be stored, the change is rolled back.

Webhook deliveries can still repeat after a network failure. Dedupe on the `X-OpenTrain-Delivery` header or on the event `id`.

## Subscribe

```bash
curl -X POST "https://www.opentrain.ai/api/public/v1/webhooks" \
  -H "Authorization: Bearer $OPENTRAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/opentrain-webhook",
    "eventTypes": [
      "project_todo.quiz.attempt_submitted",
      "project_todo.quiz.grading_required",
      "project_todo.quiz.todo_completed"
    ]
  }'
```

```bash
opentrain webhooks create --url https://example.com/opentrain-webhook \
  --events project_todo.quiz.attempt_submitted,project_todo.quiz.grading_required,project_todo.quiz.todo_completed --json
```

In MCP, call the webhook create tool with the same `eventTypes`. The quiz capabilities document (`GET /project-todos/quizzes/authoring/capabilities`, `opentrain todos quiz capabilities`, or the `opentrain_quiz_authoring_capabilities` tool) lists all nine types under `workflows.events`.

## Example event

```json
{
  "id": "981245",
  "type": "project_todo.quiz.graded",
  "apiVersion": "v1",
  "createdAt": "2026-11-02T10:15:04.000Z",
  "resourceId": "assignment_123",
  "jobId": "job_456",
  "data": {
    "assignmentId": "assignment_123",
    "bindingId": "binding_789",
    "formId": "form_321",
    "formVersionId": "version_654",
    "attemptId": "attempt_987",
    "attemptNumber": 1,
    "revisionNumber": 2,
    "gradeState": "FINALIZED",
    "outcome": "PASSED",
    "afterRelease": false
  }
}
```

Selective coaching has its own `project_todo.quiz.coaching.*` events. See [coaching](https://www.opentrain.ai/docs/developers/api-reference/project-todos/quizzes/coaching/).
