Webhooks are on the Roadmap. Until they ship, poll the submissions feed as described here. A worker built this way switches to webhooks easily, because both deliver “this submission changed, go and read it”.
The submissions feed
GET /submissions lists every submission of your institute that changed after updated_since, across all exams, oldest change first.
Response (abridged)
string
required
An ISO 8601 UTC time, for example
2026-10-01T09:00:00Z. Only submissions whose updated_at is later are returned. Required on this endpoint; for the first run, use a time before your first submission.string
The
next_cursor of the previous page, passed back unchanged. An altered or invalid cursor returns 400 invalid_cursor.integer
default:"50"
Page size, 1 to 200.
string
Optional filter, comma-separated:
queued, processing, reading, grading, graded, partially_graded, failed, cancelled.boolean
Optional filter.
boolean
Optional filter.
updated_at, then id, so paging with next_cursor never skips a row. A submission that changes while you page moves to the end and shows up again, which is what you want. Each row is a submission status, not the full result. Read the result separately when you need marks.
What moves a submission in the feed
A submission’supdated_at changes, and it appears in the feed again, whenever something you can see about it changes:
- grading progress:
queuedtoprocessing,reading,grading, thengraded,partially_graded,failedorcancelled - a teacher changing marks, on the dashboard or through the API, or approving the copy
- re-evaluation, finalize and unfinalize
- the submission being replaced or deleted
state on every row:
The worker
The loop:- Load your checkpoint, the latest
updated_atyou have fully processed. - Request the feed from a little before the checkpoint, and follow
next_cursoruntilhas_moreisfalse. - Process each row idempotently, skipping changes you have already applied.
- Save the new checkpoint, then sleep and repeat.
Start each poll a little before your checkpoint. A change that was being saved while you polled can carry a timestamp slightly earlier than rows you already received. Starting each poll one to two minutes before the checkpoint, and skipping rows you have already applied, guarantees nothing slips through.
Processing idempotently
The same change can reach you more than once: the overlap window replays recent rows, a row that changes while you page through the feed appears again on a later page, and a crashed worker repeats its last batch. Make every write safe to repeat:- Key everything by submission
id. Upsert, never insert blindly. - Remember the
updated_atyou last applied for each submission, and skip a row that is not newer. - Save the checkpoint only after the rows are applied. If the worker dies halfway, the next run starts from the old checkpoint and repeats the work, which is harmless.
- Link results to your own records with
metadata(your answer-sheet or attempt ID, up to 2 KB) or the candidate’sexternal_id.
How often to poll
Every poll counts towards your rate limit. On the standard tier, one key can make 20 reads per second and 600 per minute. Responses carry
RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers. See Going live.
Backoff and errors
Reading a whole exam at once
When a teacher finishes reviewing, you usually want every result of one exam.GET /exams/{id}/results returns full results (the same shape as GET /submissions/{id}/result) for the exam’s live submissions, so you don’t need one request per student.
limitis 1 to 50, default 20. Page withcursorandnext_cursor.updated_sincereturns only results that changed since your last read.finalized=truereturns only final marks;finalized=falseonly drafts.- Results are JSON.
format=csvreturns422 feature_not_availablefor now.
GET /exams/{id}/submissions?include=result works too, if you also want filters such as status or needs_review (limit is capped at 50 with include=result).
Next steps
Going live
The checklist before your first real exam.
Roadmap
Webhooks and what else is coming.