Skip to main content
Networks fail. A request can time out after Evalezy has already created the submission, and your retry must not create a second one or charge twice. The API gives you two tools for this:
  1. The Idempotency-Key header on POST requests: a retry with the same key returns the first answer instead of running again.
  2. Natural keys enforced by the API itself: your external_ref for exams, your external_id for candidates, and one live submission per candidate per exam.
Use both. The header covers every POST; the natural keys protect you even when a retry is sent without it, for example after your own process restarts.

The Idempotency-Key header

Send a unique value, such as a UUID, in the Idempotency-Key header. Reuse the same value for every retry of that request.

Rules

What happens on a retry

A replayed error carries a fresh request_id for the retry, not the original one.

Which outcomes are stored

This means a retry after a 500, a 503 or a 429 really retries, and a retry after topping up credits goes through, while a retry of a successful create gives you back the same resource.
A stored 422 is replayed as long as the request is identical. Once you fix the body it is a different request, so send it with a new Idempotency-Key; the old key would answer 422 idempotency_key_reused.

Endpoints that accept the header

Every POST that creates or changes something: The read-only POST lookups (/exams/search, /candidates/search, /credits/quote) are safe to repeat and ignore the header. PUT and PATCH set values, so repeating them gives the same result. A repeated DELETE answers 404, which you can treat as success.
Replays of POST /uploads never contain the upload URLs. Presigned URLs are credentials, so they are not stored. A replayed response lists the same uploads with upload_url_redacted: true and no upload_url. If you lost the URLs, create new uploads with a new Idempotency-Key.

Natural keys

These rules hold whether or not you send an Idempotency-Key.
Give every exam your own reference, for example "external_ref": "CBSE-X-SCI-HY-2026-10A". A second POST /exams with the same external_ref is refused:
Treat exam_exists as success and continue with details.exam_id. To look exams up by your references without creating anything, call POST /exams/search with up to 500 external_refs. Deleting an exam frees its external_ref for reuse.
Candidates are keyed by your student id, external_id, within your institute. POST /candidates (and inline candidate objects on submissions) upsert: the first call creates the candidate, later calls update the fields you send and keep the ones you leave out. Sending the same candidate twice never creates two.
An exam holds at most one live submission per candidate. A second POST /exams/{id}/submissions for the same candidate is refused with 409 submission_exists, and details.submission_id points to the existing one, so a blind retry can never grade a copy twice.To deliberately replace a copy (for example after a rescan), send "replace": true. The old submission is marked replaced and its grading is cancelled. A finalized submission cannot be replaced until you unfinalize it (409 submission_finalized).
Each upload can be attached to one submission only. Reusing it answers 409 upload_already_used with the submission that holds it.

A robust pattern

1

Derive keys from your own records

Use your exam id as external_ref and your student id as external_id. Retries then line up on their own.
2

Generate one Idempotency-Key per job, store it, reuse it

Persist the key next to the job in your database before the first attempt, so a restarted worker retries with the same value.
3

Retry only what is retryable

429, 500, 503 and 409 request_in_progress, honouring Retry-After. See Errors.
4

Treat natural-key conflicts as success

On exam_exists or submission_exists, read the id from details and carry on.