{"openapi":"3.1.0","info":{"title":"Acadine API","description":"Partner integration API for question papers, tests and answer-sheet submission. Authenticate every request with the API key issued to your organisation, sent in the `X-API-Key` header.","version":"1.0.0"},"paths":{"/v1/courses":{"get":{"tags":["Courses"],"summary":"List courses","description":"Courses available to your organisation. Paginated — keep requesting with the returned `offset_key` until it comes back `null`. Fetch once and store the mapping.","operationId":"list_courses_v1_courses_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Limit"}},{"name":"offset_key","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offset Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCoursesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/courses/{course_id}/subjects":{"get":{"tags":["Courses"],"summary":"List subjects in a course","description":"Subjects belonging to a course. Not paginated — a course carries a small fixed set of subjects. Returns 404 if the course does not belong to your organisation.","operationId":"list_subjects_v1_courses__course_id__subjects_get","parameters":[{"name":"course_id","in":"path","required":true,"schema":{"type":"string","title":"Course Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SubjectResponse"},"title":"Response List Subjects V1 Courses  Course Id  Subjects Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/question-papers":{"get":{"tags":["Question Papers"],"summary":"List question papers","description":"Metadata for your organisation's question papers, most recently updated first. The questions themselves are not included.\n\nCourses and subjects are returned as ids — resolve their names once via `GET /v1/courses` and `GET /v1/courses/{course_id}/subjects` and keep the mapping.\n\nPaginated by page number — request `page=2`, `page=3` and so on until `has_next_page` is false. Optionally filter by course, subject or status.","operationId":"list_question_papers_v1_question_papers_get","parameters":[{"name":"course_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one course.","title":"Course Id"},"description":"Restrict to one course."},{"name":"subject_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one subject.","title":"Subject Id"},"description":"Restrict to one subject."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/QuestionPaperStatusValue"},{"type":"null"}],"description":"Restrict to one lifecycle status.","title":"Status"},"description":"Restrict to one lifecycle status."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListQuestionPapersResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/question-papers/{question_paper_id}":{"get":{"tags":["Question Papers"],"summary":"Get a question paper","description":"Metadata for a single paper — the same fields `GET /v1/question-papers` returns for each item. The questions themselves are not included.\n\nReturns 404 if the paper does not belong to your organisation.","operationId":"get_question_paper_v1_question_papers__question_paper_id__get","parameters":[{"name":"question_paper_id","in":"path","required":true,"schema":{"type":"string","title":"Question Paper Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuestionPaperListItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/question-papers/initiate-upload":{"post":{"tags":["Question Papers"],"summary":"Start a question paper upload","description":"Step 1 of 2. Returns presigned URLs to upload your PDFs to. No question paper is created and nothing is charged until you confirm.\n\n**Flow**\n1. Call this endpoint.\n2. `PUT` each PDF to the URL(s) returned, sending `Content-Type: application/pdf`.\n3. Call `POST /v1/question-papers/confirm-upload` with the `upload_ref`.\n4. Poll `GET /v1/question-papers/{id}` until the status is `GENERATED`.\n\n**`model_answer`** — where the answers come from. Optional, defaults to `auto`. Exactly one of:\n\n| Value | Meaning | PDFs to upload |\n| --- | --- | --- |\n| `auto` | You are not supplying answers; they are worked out for you. | 1 |\n| `in_question_paper` | The answer key is inside the question paper PDF, e.g. an answer section at the end. | 1 |\n| `separate_pdf` | The answer key is its own file. A second upload URL is returned and must also be uploaded to. | 2 |\n\nWith `in_question_paper` or `separate_pdf` your answers are used verbatim and are never overwritten. A separate marking-scheme PDF is not supported yet — marks come from the answer key when you supply one.\n\nURLs expire 10 minutes after issue; if that passes, just call this again.","operationId":"initiate_question_paper_upload_v1_question_papers_initiate_upload_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateQuestionPaperUploadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateQuestionPaperUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/question-papers/confirm-upload":{"post":{"tags":["Question Papers"],"summary":"Finish a question paper upload","description":"Step 2 of 2. Call once every URL from initiate-upload has been uploaded to. Creates the question paper and queues it for extraction.\n\nExtraction is asynchronous — the paper comes back `QUEUED`. Poll `GET /v1/question-papers/{id}`: it becomes `GENERATED` when the questions are ready, or `FAILED` if the PDF could not be read. Name, marks, duration and language are all read from the PDF.\n\nSafe to retry: calling it twice with the same `upload_ref` returns the paper already created rather than creating a second one.","operationId":"confirm_question_paper_upload_v1_question_papers_confirm_upload_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuestionPaperUploadRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuestionPaperUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/review-sessions":{"post":{"tags":["Question Papers"],"summary":"Start a teacher review, or view an approved paper","description":"Get a teacher to a review/approval screen with no Acadine credential. The paper must be READY (`GENERATED`) or already `APPROVED` — the latter gets you a no-credential URL to view a paper that's already been signed off (e.g. to embed it for display) instead of re-opening the review flow. A paper still processing returns 409.\n\n**Flow**\n1. Call this endpoint.\n2. Redirect the browser to the returned `url`.\n3. Acadine redeems the token, starts a 12h session, and renders the paper — no sign-in required.\n4. For a `GENERATED` paper, approving it redirects to `return_url` (if supplied) and sends a `question_paper.approved` webhook. For an already-`APPROVED` paper, there's nothing further to approve — it's a read-only view.\n\nThe `rt=` token in `url` is single-use and expires in 120 seconds — call this again for a fresh one if it isn't used in time.","operationId":"create_review_session_v1_review_sessions_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReviewSessionRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReviewSessionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tests":{"get":{"tags":["Tests"],"summary":"List tests","description":"Every test in your organisation, most recently updated first, including those created in the Acadine dashboard rather than through this API. Deleted tests are never returned.\n\nNarrow the list with `course_id`, `subject_id`, `question_paper_id` or `status`. Page with `page` and `size`; keep going while `pagination.has_next_page` is true.","operationId":"list_tests_v1_tests_get","parameters":[{"name":"course_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one course.","title":"Course Id"},"description":"Restrict to one course."},{"name":"subject_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to one subject.","title":"Subject Id"},"description":"Restrict to one subject."},{"name":"question_paper_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Restrict to tests backed by one question paper.","title":"Question Paper Id"},"description":"Restrict to tests backed by one question paper."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/TestStatusValue"},{"type":"null"}],"description":"Restrict to one lifecycle status.","title":"Status"},"description":"Restrict to one lifecycle status."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTestsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Tests"],"summary":"Create a test","description":"Create a sitting of an approved question paper. One paper can back multiple tests.\n\nThe paper must already be `APPROVED` — approve it in the Acadine dashboard first. A paper in any other status is rejected with 409 rather than approved automatically, so machine-extracted questions are never put in front of students unreviewed.\n\nBy default the test is scheduled for **every batch on the question paper's course**. Pass `batch_ids` to narrow it to specific batches; each must be on that same course. Scheduling a test for a batch notifies its students, so pass an explicit list if the default is wider than you intend. The batches actually used come back on the response.\n\nA test is always scheduled for at least one batch: an empty `batch_ids` is rejected, and a course with no batches gets one named `Default` created for it.\n\n**Scheduling and timing**\n\nEvery test runs over an availability window: `starts_at` and `ends_at` are both required, and `ends_at` must be after `starts_at`. The window may be in the past, so a test that has already been sat can be recorded after the fact.\n\n`duration_mins` is what makes the test timed. It is a *per-student* clock that starts when each student starts, so the window says when the test is open and `duration_mins` says how long any one student gets:\n\n| Window | `duration_mins` | Result |\n| --- | --- | --- |\n| 09:00–12:00 | omitted | Three hours, open to all — students may use the whole window. |\n| 09:00–12:00 | `60` | A one-hour test that can be started any time between 09:00 and 12:00. |\n\nAn attempt ends at whichever comes first: the student's own clock or `ends_at`.\n\n**How students answer**\n\nPick at most one of these — a test is answered one way, so combining them is rejected:\n\n| Flag | How answers reach us |\n| --- | --- |\n| _(none set)_ | Students write on paper; **you** upload the scanned sheets. |\n| `is_cbt` | Students answer on screen in the app. Nothing is scanned. |\n| `is_omr` | Students fill a paper bubble sheet; you upload the scans. |\n| `allow_student_answer_sheet_upload` | Students photograph and upload their own sheets. |","operationId":"create_test_v1_tests_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTestRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTestResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tests/{test_id}":{"get":{"tags":["Tests"],"summary":"Get a test","description":"One test by id, in the same shape as a listed one. Returns 404 if the test does not exist, has been deleted, or belongs to another organisation.","operationId":"get_test_v1_tests__test_id__get","parameters":[{"name":"test_id","in":"path","required":true,"schema":{"type":"string","title":"Test Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestListItem"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tests/{test_id}/results":{"get":{"tags":["Tests"],"summary":"List every student's result for a test","description":"One page of student results for a test, in the same shape as `GET /v1/tests/{test_id}/results/{student_ref}`.\n\nOnly students with an attempt on the test appear here — someone never assigned or who never started has no attempt and so no row, same as the single-student endpoint's 404 for a student with no attempt.\n\nNarrow with `status` (`NOT_STARTED`, `QUEUED`, `UNDER_EVALUATION`, `EVALUATED`, `FAILED`, `DISPUTED`). Page with `page` and `size`; keep going while `pagination.has_next_page` is true.\n\nReturns 404 if the test doesn't exist or belongs to another organisation.","operationId":"list_test_results_v1_tests__test_id__results_get","parameters":[{"name":"test_id","in":"path","required":true,"schema":{"type":"string","title":"Test Id"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/TestResultStatusValue"},{"type":"null"}],"description":"Restrict to one evaluation status.","title":"Status"},"description":"Restrict to one evaluation status."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTestResultsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/tests/{test_id}/results/{student_ref}":{"get":{"tags":["Tests"],"summary":"Get a student's result for a test","description":"A student's result for a test, fetchable on demand rather than relying solely on the `test_result.evaluated` webhook.\n\nReturns 200 even while evaluation is still in progress — check `status` and keep polling until it's `EVALUATED` or `DISPUTED`, at which point `marks_obtained`, `evaluated_at`, `report_card_url` and `graded_answersheet_url` are populated.\n\nReturns 404 if the test doesn't exist, `student_ref` doesn't match any student in your organisation, or that student has no attempt at this test at all.","operationId":"get_test_result_v1_tests__test_id__results__student_ref__get","parameters":[{"name":"test_id","in":"path","required":true,"schema":{"type":"string","title":"Test Id"}},{"name":"student_ref","in":"path","required":true,"schema":{"type":"string","title":"Student Ref"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetTestResultResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/answer-sheets/initiate-upload":{"post":{"tags":["Answer Sheets"],"summary":"Initiate an answer sheet upload","description":"Step 1 of 2. Returns a presigned URL to upload one student's scanned answer sheet to.\n\n1. Call this with the test and the student.\n2. `PUT` the PDF to `upload_url` with `Content-Type: application/pdf`.\n3. Call `POST /v1/answer-sheets/confirm-upload` with the `upload_ref`.\n\n**Identifying the student**\n\nSend either `student_id` (an Acadine student you already have) or `student` (your own reference). With `student`, the first upload for a `student_ref` creates the student on the test's course; later uploads for the same ref reuse them, and `student_name` / `batch_ref` are then ignored rather than applied as updates.\n\nStudents created this way have no email address and cannot sign in — they exist so their sheets can be evaluated. Add an email in the Acadine dashboard if they need access.\n\nOnly for tests whose answers are on paper. A CBT test is answered on screen, so it has nothing to upload and is rejected.","operationId":"initiate_answer_sheet_upload_v1_answer_sheets_initiate_upload_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerInitiateAnswerSheetUploadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerInitiateAnswerSheetUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/answer-sheets/confirm-upload":{"post":{"tags":["Answer Sheets"],"summary":"Confirm an answer sheet upload","description":"Step 2 of 2. Call once the PDF has been uploaded. Verifies it arrived, records it against the student's attempt, and queues evaluation.\n\nTakes only the `upload_ref` — the test and the student were fixed when you initiated, so neither is re-sent.\n\nSafe to retry: repeating a confirmed `upload_ref` returns the same attempt rather than evaluating again. To genuinely replace a student's sheet, initiate a fresh upload — that re-runs evaluation and consumes credits again, and is refused only while an evaluation is in flight.\n\nEvaluation is asynchronous — results arrive as a `test.result` webhook.","operationId":"confirm_answer_sheet_upload_v1_answer_sheets_confirm_upload_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerConfirmAnswerSheetUploadRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerConfirmAnswerSheetUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ActorResponse":{"properties":{"id":{"type":"string","title":"Id","examples":["USR_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","examples":["Anita Sharma"]}},"type":"object","required":["id","name"],"title":"ActorResponse","description":"A user who acted on the paper — created it, approved it."},"ConfirmQuestionPaperUploadRequest":{"properties":{"upload_ref":{"type":"string","title":"Upload Ref","description":"The `upload_ref` from initiate-upload.","examples":["QPU_2msnplIgOEXImjfvuihUqhIqNJO"]}},"type":"object","required":["upload_ref"],"title":"ConfirmQuestionPaperUploadRequest","description":"Step 2 of 2. Call once every upload URL has been PUT to."},"ConfirmQuestionPaperUploadResponse":{"properties":{"question_paper_id":{"type":"string","title":"Question Paper Id","description":"The created paper. Poll `GET /v1/question-papers/{id}` for its status.","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"status":{"$ref":"#/components/schemas/QuestionPaperStatusValue","description":"Always `QUEUED` — extraction has been accepted but has not started yet."}},"type":"object","required":["question_paper_id","status"],"title":"ConfirmQuestionPaperUploadResponse"},"CourseResponse":{"properties":{"id":{"type":"string","title":"Id","examples":["ORG_CRS_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","examples":["JEE Main 2026"]}},"type":"object","required":["id","name"],"title":"CourseResponse"},"CreateReviewSessionRequest":{"properties":{"question_paper_ref":{"type":"string","title":"Question Paper Ref","description":"Must be READY or APPROVED. See `GET /v1/question-papers/{id}`.","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"actor_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Actor Ref","description":"Opaque teacher identifier, recorded in the audit trail and echoed back on the `question_paper.approved` webhook.","examples":["teacher-4471"]},"return_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Return Url","description":"Where Acadine redirects the teacher's browser after approval. `question_paper_ref` and `status=approved` are appended as query params so your landing page has something to render immediately — but the `question_paper.approved` webhook, not this redirect, is the authoritative signal; a closed tab or network hiccup can still leave the redirect undelivered.","examples":["https://partner.example.com/papers/4471"]}},"type":"object","required":["question_paper_ref"],"title":"CreateReviewSessionRequest","description":"Step 2 — get a teacher to a review/approval screen with no Acadine credential.\n\nThe paper must be READY (`GENERATED`) or already `APPROVED` — the latter gets you\na no-credential URL to view a paper that's already been signed off (e.g. to embed\nit for display) rather than to re-approve it. A paper still `PROCESSING` returns\n409."},"CreateReviewSessionResponse":{"properties":{"url":{"type":"string","title":"Url","description":"Redirect the teacher's browser here.","examples":["https://app.acadine.com/review?rt=rt_9f2a..."]},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"The `rt=` token in `url` stops working at this time (120 seconds from issue) and can only be redeemed once. If it passes unused, call this endpoint again for a fresh one."}},"type":"object","required":["url","expires_at"],"title":"CreateReviewSessionResponse"},"CreateTestRequest":{"properties":{"question_paper_id":{"type":"string","title":"Question Paper Id","description":"From `GET /v1/question-papers`. Must be `APPROVED` — a paper in any other status is rejected with 409. Papers are approved in the Acadine dashboard.","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","description":"Display name for this sitting.","examples":["Physics Term 1 — Morning"]},"batch_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Batch Ids","description":"Which batches sit this test. Omit to schedule it for **every batch on the course** the question paper belongs to. Pass an explicit list to narrow it — each id must be a batch on that same course, or the request is rejected. The list cannot be empty: a test is always scheduled for at least one batch.\n\nStudents in the scheduled batches are notified that the test has been scheduled.\n\nIf the course has no batches at all, one named `Default` is created and the test is scheduled for it, so the call still succeeds.","examples":[["BCH_2msnplIgOEXImjfvuihUqhIqNJO"]]},"starts_at":{"type":"string","format":"date-time","title":"Starts At","description":"When the test opens. Students may begin any time from here until `ends_at`. May be in the past — useful for recording a test that has already been sat."},"ends_at":{"type":"string","format":"date-time","title":"Ends At","description":"When the test closes — a hard cutoff for everyone, however late they started. Must be after `starts_at`, and may also be in the past."},"duration_mins":{"anyOf":[{"type":"integer","minimum":1.0},{"type":"null"}],"title":"Duration Mins","description":"How long each student gets, in minutes — this is what makes the test *timed*. The clock is personal: it starts when that student starts, not when the window opens, so students who begin at different times each get the full duration. Their attempt ends at whichever comes first, their own clock running out or the window closing at `ends_at`.\n\nOmit it and there is no personal clock — students may use the window right up to `ends_at`. So a 3-hour window with `duration_mins: 60` is a one-hour test students can start any time within three hours; the same window without it is a three-hour test.\n\nMust be at least 1 and cannot exceed the window length (`ends_at` − `starts_at`) — a longer value is rejected, since a student could never use the extra time.","examples":[180]},"is_cbt":{"type":"boolean","title":"Is Cbt","description":"Students answer on screen, in the app — nothing is written or scanned. Answers are submitted digitally and graded immediately, so the paper must be entirely multiple choice, multiple select, or numeric-answer questions; a paper containing anything that cannot be auto-graded is rejected.","default":false},"is_omr":{"type":"boolean","title":"Is Omr","description":"Students fill in a paper OMR (bubble) sheet, which you scan and upload. Grading reads the darkened bubbles rather than marking written answers.","default":false},"shuffle_questions":{"type":"boolean","title":"Shuffle Questions","description":"Shuffle question order per student, so no two see the same sequence. The question paper itself is unchanged.","default":false},"allow_student_answer_sheet_upload":{"type":"boolean","title":"Allow Student Answer Sheet Upload","description":"Students photograph and upload their own handwritten answer sheets, instead of you uploading scans on their behalf. Uploads are collected until the test closes at `ends_at` and evaluated together shortly after.","default":false},"external_test_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Test Ref","description":"Your own identifier for this test. Opaque to Acadine — stored as-is and echoed back on `GET /v1/tests/{test_id}` and `GET /v1/tests/{test_id}/results/{student_ref}`, so you don't have to cross-reference our `test_id` for support/reconciliation.","examples":["their-test-4471"]}},"type":"object","required":["question_paper_id","name","starts_at","ends_at"],"title":"CreateTestRequest","description":"A test is a sitting of an approved question paper. One paper can back many tests.\n\nBy default the test is scheduled for every batch on the question paper's course;\n`batch_ids` narrows that to specific batches.\n\nEvery test is available over a window: `starts_at` and `ends_at` are both required.\nThe window may be in the past, so a test that has already been sat can be recorded.","examples":[{"duration_mins":180,"ends_at":"2026-03-14T12:00:00Z","name":"Physics Term 1 — Morning","question_paper_id":"QPR_2msnplIgOEXImjfvuihUqhIqNJO","starts_at":"2026-03-14T09:00:00Z"}]},"CreateTestResponse":{"properties":{"test_id":{"type":"string","title":"Test Id","examples":["TST_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","examples":["Physics Term 1 — Morning"]},"status":{"$ref":"#/components/schemas/TestStatusValue"},"question_paper_id":{"type":"string","title":"Question Paper Id","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"course_id":{"type":"string","title":"Course Id","examples":["ORG_CRS_2msnplIgOEXImjfvuihUqhIqNJO"]},"subject_id":{"type":"string","title":"Subject Id","examples":["ORG_SUB_2msnplIgOEXImjfvuihUqhIqNJO"]},"total_marks":{"type":"number","title":"Total Marks","description":"Copied from the question paper.","examples":[100]},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"},"duration_mins":{"type":"integer","title":"Duration Mins","description":"The per-student clock actually applied — the full window length when none was given.","examples":[180]},"batch_ids":{"items":{"type":"string"},"type":"array","title":"Batch Ids","description":"The batches this test is scheduled for.","examples":[["BCH_2msnplIgOEXImjfvuihUqhIqNJO"]]},"is_cbt":{"type":"boolean","title":"Is Cbt"},"is_omr":{"type":"boolean","title":"Is Omr"},"shuffle_questions":{"type":"boolean","title":"Shuffle Questions"},"allow_student_answer_sheet_upload":{"type":"boolean","title":"Allow Student Answer Sheet Upload"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"created_by":{"anyOf":[{"$ref":"#/components/schemas/ActorResponse"},{"type":"null"}],"description":"Who created the test. `Partner API` for tests created through this API."},"external_test_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Test Ref","description":"Your own identifier for this test, if you set one at creation.","examples":["their-test-4471"]}},"type":"object","required":["test_id","name","status","question_paper_id","course_id","subject_id","total_marks","duration_mins","batch_ids","is_cbt","is_omr","shuffle_questions","allow_student_answer_sheet_upload","created_at"],"title":"CreateTestResponse","description":"The created test. Same shape as a listed one — fields you did not set, such as\n`duration_mins` and `batch_ids`, come back resolved to what was actually applied."},"GetTestResultResponse":{"properties":{"status":{"$ref":"#/components/schemas/TestResultStatusValue","description":"Where evaluation currently stands. `marks_obtained`, `evaluated_at`, `report_card_url` and `graded_answersheet_url` are only populated once this is EVALUATED or DISPUTED — poll until then."},"answer_sheet_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Answer Sheet Ref","description":"The stored answer sheet this result is for. Null for a CBT/online attempt with no scanned sheet.","examples":["TAS_5c1e77b9"]},"test_id":{"type":"string","title":"Test Id","examples":["TST_2f8a91c4"]},"external_test_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Test Ref","description":"Your own identifier for the test, if you set one at creation."},"student_ref":{"type":"string","title":"Student Ref","examples":["a7f3-9c21"]},"marks_obtained":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Marks Obtained","description":"Marks the student scored.","examples":[72]},"total_marks":{"type":"number","title":"Total Marks","description":"What the test is out of.","examples":[100]},"evaluated_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Evaluated At"},"report_card_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Report Card Url","description":"Public, no-login report card page for this attempt. No expiry."},"graded_answersheet_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Graded Answersheet Url","description":"Presigned link to the annotated/marked-up answer sheet PDF. Only present for an offline/scanned attempt once annotation has finished — null for a CBT/online attempt (nothing to scan) or before annotation completes. Expires; re-fetch this endpoint for a fresh one rather than caching it."}},"type":"object","required":["status","test_id","student_ref","total_marks"],"title":"GetTestResultResponse","description":"The same data `test.result` webhooks carry, fetchable on demand — for a partner\nthat missed a webhook delivery, or that would rather poll than run a receiver."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InitiateQuestionPaperUploadRequest":{"properties":{"course_id":{"type":"string","title":"Course Id","description":"From `GET /v1/courses`.","examples":["ORG_CRS_2msnplIgOEXImjfvuihUqhIqNJO"]},"subject_id":{"type":"string","title":"Subject Id","description":"Must belong to `course_id` — see `GET /v1/courses/{course_id}/subjects`. A subject from a different course returns 404.","examples":["ORG_SUB_2msnplIgOEXImjfvuihUqhIqNJO"]},"model_answer":{"$ref":"#/components/schemas/ModelAnswerSource","description":"One of `auto`, `in_question_paper` or `separate_pdf` — see the ModelAnswerSource schema below for what each means. Defaults to `auto`. Only `separate_pdf` causes a second upload URL to be returned.","default":"auto"}},"type":"object","required":["course_id","subject_id"],"title":"InitiateQuestionPaperUploadRequest","description":"Step 1 of 2. Reserves S3 space and hands back the URLs to upload to.\n\nNo paper is created and nothing is charged at this point — that happens on confirm.","examples":[{"course_id":"ORG_CRS_2msnplIgOEXImjfvuihUqhIqNJO","model_answer":"auto","subject_id":"ORG_SUB_2msnplIgOEXImjfvuihUqhIqNJO"}]},"InitiateQuestionPaperUploadResponse":{"properties":{"upload_ref":{"type":"string","title":"Upload Ref","description":"Pass this back to `POST /v1/question-papers/confirm-upload`.","examples":["QPU_2msnplIgOEXImjfvuihUqhIqNJO"]},"question_paper_upload_url":{"type":"string","title":"Question Paper Upload Url","description":"PUT the question paper PDF here. Send `Content-Type: application/pdf` — the header is part of the signature and the upload is rejected without it."},"model_answer_upload_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Answer Upload Url","description":"PUT the answer key PDF here, same rules as above. Present only when `model_answer` is `separate_pdf`; `null` otherwise."},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"Every URL above stops working at this time (10 minutes from issue). If it passes, call initiate-upload again for fresh URLs."}},"type":"object","required":["upload_ref","question_paper_upload_url","expires_at"],"title":"InitiateQuestionPaperUploadResponse"},"ListCoursesResponse":{"properties":{"courses":{"items":{"$ref":"#/components/schemas/CourseResponse"},"type":"array","title":"Courses"},"pagination":{"$ref":"#/components/schemas/Pagination"}},"type":"object","required":["courses","pagination"],"title":"ListCoursesResponse"},"ListQuestionPapersResponse":{"properties":{"question_papers":{"items":{"$ref":"#/components/schemas/QuestionPaperListItem"},"type":"array","title":"Question Papers"},"pagination":{"$ref":"#/components/schemas/PageInfo"}},"type":"object","required":["question_papers","pagination"],"title":"ListQuestionPapersResponse"},"ListTestResultsResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/GetTestResultResponse"},"type":"array","title":"Results"},"pagination":{"$ref":"#/components/schemas/PageInfo"}},"type":"object","required":["results","pagination"],"title":"ListTestResultsResponse","description":"One page of student results for a test — same per-student shape as\n`GET /v1/tests/{test_id}/results/{student_ref}`."},"ListTestsResponse":{"properties":{"tests":{"items":{"$ref":"#/components/schemas/TestListItem"},"type":"array","title":"Tests"},"pagination":{"$ref":"#/components/schemas/PageInfo"}},"type":"object","required":["tests","pagination"],"title":"ListTestsResponse"},"ModelAnswerSource":{"type":"string","enum":["auto","in_question_paper","separate_pdf"],"title":"ModelAnswerSource","description":"Where the answers to the questions come from. One of three values:\n\n- `auto` — you are not supplying answers. Answers and solutions are worked out\n  for you. Only the question paper PDF is uploaded.\n- `in_question_paper` — the answer key is inside the question paper PDF itself,\n  for example an answer section at the end. Still only one PDF to upload.\n- `separate_pdf` — the answer key is its own file. `initiate-upload` returns a\n  second URL which **must** also be uploaded to before you confirm.\n\nWith `in_question_paper` or `separate_pdf` your answers are used verbatim and are\nnever overwritten. A separate marking-scheme PDF is not supported yet: marks are\nread from the answer key when you supply one, and worked out otherwise."},"PageInfo":{"properties":{"page":{"type":"integer","title":"Page","examples":[1]},"size":{"type":"integer","title":"Size","examples":[50]},"total_count":{"type":"integer","title":"Total Count","description":"Total papers matching the filters.","examples":[137]},"total_pages":{"type":"integer","title":"Total Pages","examples":[3]},"has_next_page":{"type":"boolean","title":"Has Next Page"}},"type":"object","required":["page","size","total_count","total_pages","has_next_page"],"title":"PageInfo"},"Pagination":{"properties":{"offset_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Offset Key","description":"Pass back as `offset_key` to fetch the next page. `null` on the last page — keep requesting until it comes back `null`."}},"type":"object","title":"Pagination"},"PartnerConfirmAnswerSheetUploadRequest":{"properties":{"upload_ref":{"type":"string","title":"Upload Ref","description":"The `upload_ref` from initiate-upload.","examples":["ASU_2msnplIgOEXImjfvuihUqhIqNJO"]}},"type":"object","required":["upload_ref"],"title":"PartnerConfirmAnswerSheetUploadRequest","description":"Step 2 of 2. Call once the PDF has been PUT to the upload URL."},"PartnerConfirmAnswerSheetUploadResponse":{"properties":{"answer_sheet_ref":{"type":"string","title":"Answer Sheet Ref","description":"The stored answer sheet. Referenced by the `test.result` webhook.","examples":["TAS_5c1e77b9"]},"student_id":{"type":"string","title":"Student Id","description":"The Acadine student this sheet was recorded against.","examples":["USR_9d21f0ab"]},"status":{"type":"string","title":"Status","description":"Attempt status once submitted for evaluation.","examples":["SUBMITTED"]}},"type":"object","required":["answer_sheet_ref","student_id","status"],"title":"PartnerConfirmAnswerSheetUploadResponse"},"PartnerInitiateAnswerSheetUploadRequest":{"properties":{"test_id":{"type":"string","title":"Test Id","description":"The test this answer sheet is for.","examples":["TST_2f8a91c4"]},"student_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Student Id","description":"An existing Acadine student id. Provide this or `student`, not both.","examples":["USR_9d21f0ab"]},"student":{"anyOf":[{"$ref":"#/components/schemas/PartnerStudentInput"},{"type":"null"}],"description":"Identify the student by your own reference instead. Provide this or `student_id`, not both."}},"type":"object","required":["test_id"],"title":"PartnerInitiateAnswerSheetUploadRequest","description":"Step 1 of 2. Identifies whose answer sheet, for which test.\n\nThe student is named either by `student_id` (an Acadine id, for partners whose\nstudents are already onboarded) or by `student` (the partner's own identifiers)."},"PartnerInitiateAnswerSheetUploadResponse":{"properties":{"upload_ref":{"type":"string","title":"Upload Ref","description":"Pass this to confirm-upload once the PDF has been uploaded.","examples":["ASU_2msnplIgOEXImjfvuihUqhIqNJO"]},"student_id":{"type":"string","title":"Student Id","description":"The Acadine student this sheet will be recorded against — newly created if this was the first upload for that `student_ref`. Store it to address the same student directly next time.","examples":["USR_9d21f0ab"]},"upload_url":{"type":"string","title":"Upload Url","description":"Presigned URL to PUT the answer sheet PDF to."},"expires_at":{"type":"string","format":"date-time","title":"Expires At","description":"After this, call initiate-upload again for a fresh URL."}},"type":"object","required":["upload_ref","student_id","upload_url","expires_at"],"title":"PartnerInitiateAnswerSheetUploadResponse"},"PartnerStudentInput":{"properties":{"student_ref":{"type":"string","minLength":1,"title":"Student Ref","description":"Your own stable identifier for this student. Stored as the student's enrollment id and used to recognise them on subsequent uploads, so it must be the same string every time you submit for this student.","examples":["a7f3-9c21"]},"student_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Student Name","description":"Full name, used only when the student is first created. Split on the first space into first and last name. Defaults to `student_ref` if omitted, and is ignored for a student who already exists — it will not rename them.","examples":["Ravi Kumar"]},"batch_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Batch Ref","description":"Which batch to place a newly created student in, matched by name against the test's course and created there if it does not exist. Omit to enrol them in every batch on the course. Ignored for a student who already exists.","examples":["JEE-2026-A"]}},"type":"object","required":["student_ref"],"title":"PartnerStudentInput","description":"A student described by the partner's own identifiers, for callers that have not\nonboarded students into Acadine. The first upload for a `student_ref` creates the\nstudent; later uploads for the same ref reuse them."},"QuestionPaperLanguageValue":{"type":"string","enum":["ENGLISH","HINDI","SANSKRIT","GUJARATI","MARATHI","BENGALI","KANNADA","ODIYA","TELUGU","TAMIL","URDU"],"title":"QuestionPaperLanguageValue","description":"Language a paper is written in. Mirrors the internal QuestionPaperLanguage."},"QuestionPaperListItem":{"properties":{"id":{"type":"string","title":"Id","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","examples":["Physics Term 1"]},"status":{"$ref":"#/components/schemas/QuestionPaperStatusValue"},"origin":{"$ref":"#/components/schemas/QuestionPaperOrigin","description":"How the paper came into existence."},"parent_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Ref","description":"The paper this one derives from. Set only when origin is CLONED or TRANSLATED.","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"language":{"$ref":"#/components/schemas/QuestionPaperLanguageValue"},"course_id":{"type":"string","title":"Course Id","examples":["ORG_CRS_2msnplIgOEXImjfvuihUqhIqNJO"]},"subject_id":{"type":"string","title":"Subject Id","examples":["ORG_SUB_2msnplIgOEXImjfvuihUqhIqNJO"]},"total_marks":{"type":"number","title":"Total Marks","examples":[100]},"total_questions":{"type":"integer","title":"Total Questions","description":"Question count for the paper. 0 while it is still being generated.","examples":[30]},"created_at":{"type":"string","format":"date-time","title":"Created At"},"created_by":{"anyOf":[{"$ref":"#/components/schemas/ActorResponse"},{"type":"null"}],"description":"Null for papers created by an automated process."},"approved_by":{"anyOf":[{"$ref":"#/components/schemas/ActorResponse"},{"type":"null"}],"description":"Null unless status is APPROVED."},"approved_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Approved At","description":"Null unless status is APPROVED."}},"type":"object","required":["id","name","status","origin","language","course_id","subject_id","total_marks","total_questions","created_at"],"title":"QuestionPaperListItem","description":"Metadata for one paper. Deliberately excludes the questions themselves —\nfetch a single paper if you need its content."},"QuestionPaperOrigin":{"type":"string","enum":["IMPORTED","GENERATED","CLONED","TRANSLATED"],"title":"QuestionPaperOrigin","description":"How a paper came into existence.\n\nCLONED and TRANSLATED papers derive from another paper rather than being authored\noutright, and carry `parent_ref` pointing at it."},"QuestionPaperStatusValue":{"type":"string","enum":["DRAFT","QUEUED","PROCESSING","GENERATED","APPROVED","FAILED"],"title":"QuestionPaperStatusValue","description":"Lifecycle of a question paper.\n\nMirrors the internal QuestionPaperStatus one-for-one — a paper being generated moves\nQUEUED -> PROCESSING -> GENERATED, and APPROVED once a teacher signs it off."},"SubjectResponse":{"properties":{"id":{"type":"string","title":"Id","examples":["ORG_SUB_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","examples":["Physics"]}},"type":"object","required":["id","name"],"title":"SubjectResponse"},"TestListItem":{"properties":{"test_id":{"type":"string","title":"Test Id","examples":["TST_2msnplIgOEXImjfvuihUqhIqNJO"]},"name":{"type":"string","title":"Name","examples":["Physics Term 1 — Morning"]},"status":{"$ref":"#/components/schemas/TestStatusValue"},"question_paper_id":{"type":"string","title":"Question Paper Id","examples":["QPR_2msnplIgOEXImjfvuihUqhIqNJO"]},"course_id":{"type":"string","title":"Course Id","examples":["ORG_CRS_2msnplIgOEXImjfvuihUqhIqNJO"]},"subject_id":{"type":"string","title":"Subject Id","examples":["ORG_SUB_2msnplIgOEXImjfvuihUqhIqNJO"]},"total_marks":{"type":"number","title":"Total Marks","description":"Copied from the question paper.","examples":[100]},"starts_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Starts At"},"ends_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Ends At"},"duration_mins":{"type":"integer","title":"Duration Mins","description":"The per-student clock actually applied — the full window length when none was given.","examples":[180]},"batch_ids":{"items":{"type":"string"},"type":"array","title":"Batch Ids","description":"The batches this test is scheduled for.","examples":[["BCH_2msnplIgOEXImjfvuihUqhIqNJO"]]},"is_cbt":{"type":"boolean","title":"Is Cbt"},"is_omr":{"type":"boolean","title":"Is Omr"},"shuffle_questions":{"type":"boolean","title":"Shuffle Questions"},"allow_student_answer_sheet_upload":{"type":"boolean","title":"Allow Student Answer Sheet Upload"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"created_by":{"anyOf":[{"$ref":"#/components/schemas/ActorResponse"},{"type":"null"}],"description":"Who created the test. `Partner API` for tests created through this API."},"external_test_ref":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Test Ref","description":"Your own identifier for this test, if you set one at creation.","examples":["their-test-4471"]}},"type":"object","required":["test_id","name","status","question_paper_id","course_id","subject_id","total_marks","duration_mins","batch_ids","is_cbt","is_omr","shuffle_questions","allow_student_answer_sheet_upload","created_at"],"title":"TestListItem","description":"One test. Returned by create, list and get alike, so the three cannot drift."},"TestResultStatusValue":{"type":"string","enum":["NOT_STARTED","QUEUED","UNDER_EVALUATION","EVALUATED","FAILED","DISPUTED"],"title":"TestResultStatusValue","description":"Mirrors the internal TestEvaluationStatus one-for-one."},"TestStatusValue":{"type":"string","enum":["CREATED","COMPLETED","CANCELLED"],"title":"TestStatusValue","description":"Lifecycle of a test. Mirrors the internal TestStatus one-for-one."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"OrgApiKey":{"type":"apiKey","in":"header","name":"X-API-Key"}}},"tags":[{"name":"Courses"},{"name":"Question Papers"},{"name":"Tests"},{"name":"Answer Sheets"}],"security":[{"OrgApiKey":[]}]}