Files
There are two ways to give us a document: upload it to us, or give us an HTTPS link to it. Uploading is the more reliable of the two.
Upload a file
Section titled “Upload a file”POST /v1/files
| Parameter | Required | Description |
|---|---|---|
purpose |
Yes | question_paper, marking_scheme or answer_script. |
filename |
Yes | The file’s name, for your reference. Up to 255 characters. |
content_type |
Yes | application/pdf, image/jpeg or image/png. See Accepted documents. |
size_bytes |
Yes | The exact size of the file in bytes. |
Response 201:
{ "id": "file_As2Vb6Nc8T", "object": "file", "livemode": true, "purpose": "answer_script", "filename": "STU-0042-physics-paper-2.pdf", "content_type": "application/pdf", "size_bytes": 7340032, "pages": null, "status": "awaiting_upload", "upload": { "url": "https://...", "method": "PUT", "headers": { "Content-Type": "application/pdf", "If-None-Match": "*" }, "expires_at": "2026-10-06T10:14:02Z" }, "created_at": "2026-10-06T09:14:02Z", "expires_at": "2026-10-07T09:14:02Z"}Then send the file’s bytes to upload.url:
curl -X PUT "<upload.url>" \ -H "Content-Type: application/pdf" \ -H "If-None-Match: *" \ --data-binary @STU-0042-physics-paper-2.pdf- Use
upload.method, and send exactly the headers inupload.headers. Don’t add your API key: the URL is already authorised. - Send exactly
size_bytesbytes. The upload is refused if the size differs. - When the
PUTreturns200, the file isuploadedand you can use it straight away. - An upload URL takes one upload. A second
PUTgets412: the first one is already stored. Retrieve the file to check that it isuploaded. - The upload URL works until
upload.expires_at, one hour after creation. After that, create a new file. - The upload URL belongs to our storage service. Its errors are short XML documents, not this API’s error format; treat any non-
200as a failed upload and create a new file. uploadis returned only when you create the file. It isnullwhen you retrieve the file later.
The File object
Section titled “The File object”| Field | Type | Description |
|---|---|---|
id |
string | Unique ID, prefix file_. |
object |
string | Always file. |
livemode |
boolean | true for live mode. |
purpose |
string | question_paper, marking_scheme or answer_script. |
filename |
string | The name you gave, or the last part of the link’s path. |
content_type |
string | The file’s format. |
size_bytes |
integer | The file’s size. |
pages |
integer | Pages in the file once we’ve read it, otherwise null. Each image counts as 1. |
status |
string | See below. |
upload |
object | Upload instructions. Only in the create response. |
created_at |
timestamp | When the file was created. |
expires_at |
timestamp | When the file is deleted if it isn’t used, or null while it is attached. |
status |
Meaning |
|---|---|
awaiting_upload |
Created, but we haven’t received the bytes yet. |
uploaded |
We have the bytes. You can use the file in a paper or submission. |
attached |
Used by a paper or submission. It is kept, and deleted, with that object. |
expired |
Not used in time, so we deleted it. |
- A file can be used by one paper or one submission at a time, and only for the purpose it was created with.
- If the paper or submission using a file fails or is cancelled, the file is released: it returns to
uploaded, and you can use it again within 24 hours. - A file that is never used is deleted 24 hours after it was created.
Retrieve a file
Section titled “Retrieve a file”GET /v1/files/{id}
Use a link instead of uploading
Section titled “Use a link instead of uploading”Wherever a request takes a document, you can pass a link instead of a file ID:
{ "paper_id": "pap_3fT9qLx2Wd", "student": { "external_id": "STU-0042" }, "files": [ { "url": "https://cdn.example.com/scripts/STU-0042.pdf?signature=..." } ]}- The link must use HTTPS and work without cookies or extra headers. A pre-signed link is ideal.
- Keep it valid for at least 1 hour after your request.
- Your server must start responding within 30 seconds. We follow up to 3 redirects, and refuse links to private or internal network addresses.
- The download must be an accepted format and within the size limit.
We download each link once, shortly after your request, and create a File from it. That File then appears in the paper’s question_paper or marking_scheme, or in the submission’s files. If a download fails, the paper or submission fails with file_download_failed, and nothing is charged.
Accepted documents
Section titled “Accepted documents”| Purpose | Formats | Per file | Per paper or submission |
|---|---|---|---|
question_paper |
Up to 100 MB and 50 pages | One file | |
marking_scheme |
Up to 100 MB and 50 pages | One file | |
answer_script |
PDF, JPEG or PNG | Up to 100 MB | One PDF, or 1 to 50 images (not both); 50 pages in total |
- We check each file’s actual contents, not its name or declared type.
- Images must be at least 200 pixels on each side, and each image must show exactly one page.
- We compress large documents before we read them; compression never changes the page count. To be read in full, a script must be under 12 MB after compression, and a paper’s question paper and marking scheme together under 15 MB. Anything larger fails with
file_too_large, and nothing is charged.
Getting good results from scans
Section titled “Getting good results from scans”- Scan at 150 to 200 dpi rather than sending full-resolution phone photos: smaller files are read faster and stay within the size limits.
- Scan or photograph each page flat, upright and in good light, with the whole page in view.
- Put one student’s pages in one submission, in page order.
- Leave out pages with nothing written on them. Every page you send is counted and charged.
- Where you can, cover students’ names on the script itself. We don’t need them to mark.