Skip to main content
Every list endpoint works the same way: rows come back ordered by when they last changed (oldest change first), in pages, with an opaque cursor to the next page. The same ordering lets you keep your system in sync by asking only for what changed since your last poll.

Page shape

array
The rows of this page, ordered by updated_at, then by id.
string | null
Pass this as cursor to get the next page. null on the last page.
boolean
true when another page follows.

Query parameters

integer
default:"50"
Rows per page, from 1 to 200. GET /exams/{id}/results is the exception: 1 to 50, default 20. A value out of range is 422 validation_failed.
string
The next_cursor of the previous page, exactly as received. It is opaque: do not build or edit it. Anything else is 400 invalid_cursor.
string
Only rows whose updated_at is strictly after this time. An ISO 8601 UTC timestamp, for example 2026-10-01T09:00:00Z. Fractional seconds are accepted, so you can pass back an updated_at value as is. A value that does not parse is 422 validation_failed.
Keep the same filters (updated_since, status and so on) on every page of one pass and add cursor. Cursors do not expire, but they are tied to the ordering, not to a snapshot: rows that change while you are paging move to the end and are served again later.

Paginated endpoints

Filter values:
  • status takes one or more submission statuses, comma-separated: queued, processing, reading, grading, graded, partially_graded, failed, cancelled.
  • needs_review and finalized take true or false.
  • include on results takes annotations and model_answer; extracted_answer is included by default and -extracted_answer leaves it out.

Reading a whole list

Keeping in sync

Evalezy does not send webhooks yet (see the Roadmap). Instead, poll the submission feed, GET /submissions?updated_since=…. A submission’s updated_at moves whenever anything you can see about it changes: queued to grading to graded, a failure, a cancellation, a teacher’s override in the dashboard, approval, finalize or unfinalize, replacement or deletion. Grading progress shows up in the feed within a few seconds. Because rows are ordered by updated_at and a changed row always moves to the end, a loop that remembers how far it got never misses a change.
1

Keep a watermark

Store the updated_at of the last row you processed. Start with a time before your first submission, for example the start of the exam day.
2

Each poll, read everything changed since the watermark

Call GET /submissions?updated_since=<watermark minus 2 minutes>&limit=200 and follow next_cursor until has_more is false. The small overlap catches rows that were committed a moment late.
3

Process idempotently

Upsert each submission by id, and skip a row whose updated_at is not newer than the one you already stored. The overlap means you will see some rows twice; this check makes that harmless.
4

Advance the watermark and sleep

Set the watermark to the newest updated_at you processed, then wait 30 to 60 seconds before the next poll.

What to do with each row

The feed returns the same submission object as GET /submissions/{id}. The fields that drive a sync:
Sync the feed without a status filter. If you only ask for status=graded, you will not notice a graded copy that a teacher later changes, a replacement, or a deletion until something else touches it.
Polling the feed once a minute uses a tiny fraction of your read limit. Prefer one feed loop for the whole institute over polling each submission.