Skip to main content
Handwritten answer sheets never pass through the API body. You ask Evalezy for a presigned upload URL, PUT the PDF straight to private storage, and then reference the upload by its id when you create a submission.
Uploads need the evaluation:write scope to create and evaluation:read to inspect. An upload belongs to your institute: no other institute can see it or use it.

What you can upload

Phone photos (image/jpeg, image/png, image/heic) are not accepted yet. A request that includes an image type is refused with 422 feature_not_available. Scan or combine the pages into a single PDF first. Image uploads are on the roadmap.

Upload a PDF step by step

1

Request an upload URL

Send the exact size of the file in bytes. The presigned URL is bound to that size and to the content type, so storage rejects a PUT of any other size.You can send one file at the top level:
Or up to 100 files in files[] (a whole class at once). Do not mix the two forms in one request.
The response is 201 Created, with one entry per file in the order you sent them:
You can also send an optional sha256 (64 hexadecimal characters) for each file. It is stored and returned on the upload for your own records.
2

PUT the file to upload_url

Use the method from the response (always PUT), send every header in headers, and send the raw PDF bytes as the body. Do not add the X-API-Key header to this request: the URL itself is the credential.
Upload URLs are bearer credentials. Keep them on your server, never log them, and never send them to a browser or mobile app. Use each URL within its hour.
3

Check the upload

Read the upload once the PUT has finished. The first read after the PUT inspects the file: it confirms the object exists, checks its size and type, and counts the pages.
A ready upload has an exact pages count, and that count is what a handwritten copy is charged on. You can skip this step and submit directly: a submission runs the same check itself. Reading the upload first lets you catch a bad scan before you create a submission.
4

Submit it

Pass the id as upload_id on POST /exams/{exam_id}/submissions.

Upload fields

string
The upload id. Use it as upload_id on a submission.
string
The filename you sent.
string
Always application/pdf.
integer
The declared size, replaced by the stored size once the file has been checked.
string
The checksum you sent, if any. Omitted otherwise.
string
pending, ready or rejected. See Upload states.
integer | null
The page count of the PDF, set when the upload is ready.
string | null
Why the upload was rejected. See Reject reasons.
string | null
The submission that used this upload, once one has.
string
When the presigned PUT URL stops working (ISO 8601, UTC).
string
When the upload was requested.

Upload states

An upload stays pending while its PUT URL is still valid and no file has arrived. Once the URL has expired with nothing uploaded, the next read rejects it with missing_object.

Reject reasons

A ready upload can still be refused when you submit it. Copies of more than 80 pages are refused with 422 too_many_pages, and copies of 41 to 80 pages are accepted but flagged for human review. See the page policy.

Quote before you submit

To see the exact cost of a set of uploads before you submit them, quote them by id. Pending uploads are checked first, so the page counts are exact.
You can quote 1 to 100 uploads per call. The quote needs only the evaluation:read scope.

Retries and idempotency

POST /uploads accepts an optional Idempotency-Key header. Because the upload URLs are credentials, they are never stored: a replay returns the same upload ids without upload_url and headers, and with "upload_url_redacted": true. If you no longer have the URLs, request new uploads with a new Idempotency-Key.

Errors

When you submit an upload, you can also get 422 upload_rejected (with details.reason: a reject reason above, not_uploaded or not_ready) or 409 upload_already_used (with details.submission_id).