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
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 Or up to 100 files in The response is You can also send an optional
PUT of any other size.You can send one file at the top level:files[] (a whole class at once). Do not mix the two forms in one request.201 Created, with one entry per file in the order you sent them: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.3
Check the upload
Read the upload once the A
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.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.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).