- The
Idempotency-Keyheader onPOSTrequests: a retry with the same key returns the first answer instead of running again. - Natural keys enforced by the API itself: your
external_reffor exams, yourexternal_idfor candidates, and one live submission per candidate per exam.
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 theIdempotency-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.
Endpoints that accept the header
EveryPOST 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 anIdempotency-Key.
Exams: external_ref
Exams: external_ref
Give every exam your own reference, for example Treat
"external_ref": "CBSE-X-SCI-HY-2026-10A". A second POST /exams with the same external_ref is refused: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: external_id
Candidates: external_id
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.Submissions: one live submission per candidate per exam
Submissions: one live submission per candidate per exam
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).Uploads: used once
Uploads: used once
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.