> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crazygoldfish.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Evaluate an exam with the API

> Evaluate handwritten exam answer sheets with the CrazyGoldFish API: upload the paper and answer key, grade answers, review scores, and publish results.

Exam Evaluation reads an exam paper, reads the answer key, then marks each student's handwritten answer sheet against it and explains every mark it takes off.

This guide is for developers. For a non-technical overview, see [Exam Evaluation features](/products/evaluation-layer/features).

The flow has eight steps. Three of them upload something, and each one follows the same four moves: create it, upload files, submit with `isFinal`, wait for the AI.

| Step | You call                                   | You get back                                 | Teacher can                |
| ---- | ------------------------------------------ | -------------------------------------------- | -------------------------- |
| 1    | Create the exam, upload the question paper | An exam ID                                   |                            |
| 2    | Get the extracted questions                | Sections, questions, and marks               | Fix questions and marks    |
| 3    | Upload the answer key                      | A model answer sheet ID                      |                            |
| 4    | Get the extracted model answers            | A model answer and step marking per question | Fix the answer key         |
| 5    | Upload a student's answer sheet            | An answer sheet ID                           |                            |
| 6    | Get the scores for review                  | Marks and feedback per question              | Change any mark            |
| 7    | Publish the results                        |                                              | Release scores to students |
| 8    | Handle student queries (optional)          | The AI's second look at an answer            | Reply in the thread        |

<Info>
  Steps 1, 3, and 5 start background AI jobs. Exams report a `status`, but model answer sheets and answer sheets do not, so you wait on the endpoint that returns the result: it answers `400` while the job runs and `200` when it is ready. The `wait_for` helper below does this for you.
</Info>

## Set up

You need an access token. If you don't have one, follow the [Quickstart](/api-reference/quickstart).

<CodeGroup>
  ```python Python theme={null}
  import time

  import requests

  BASE_URL = "https://api.crazygoldfish.com/evaluation-platform"
  HEADERS = {"Authorization": "Bearer YOUR_ACCESS_TOKEN"}


  def wait_for(path):
      """Poll a result endpoint every 5 seconds until it returns 200."""
      for _ in range(120):  # Give up after 10 minutes
          response = requests.get(BASE_URL + path, headers=HEADERS)
          if response.status_code == 200:
              return response.json()
          if response.status_code != 400:
              response.raise_for_status()
          body = response.json()
          message = body["error"]["message"]["non_field_errors"][0]
          if "failed" in message:
              raise RuntimeError(message)
          time.sleep(5)
      raise TimeoutError(f"{path} took longer than 10 minutes")
  ```

  ```javascript JavaScript theme={null}
  const BASE_URL =
    "https://api.crazygoldfish.com/evaluation-platform";
  const HEADERS = { Authorization: "Bearer YOUR_ACCESS_TOKEN" };

  // Poll a result endpoint every 5 seconds until it returns 200
  async function waitFor(path) {
    for (let i = 0; i < 120; i++) { // Give up after 10 minutes
      const response = await fetch(BASE_URL + path, {
        headers: HEADERS,
      });
      if (response.status === 200) return await response.json();
      if (response.status !== 400) {
        throw new Error(await response.text());
      }
      const body = await response.json();
      const message = body.error.message.non_field_errors[0];
      if (message.includes("failed")) throw new Error(message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
    throw new Error(`${path} took longer than 10 minutes`);
  }
  ```
</CodeGroup>

<Warning>
  Every path needs its trailing slash. Without it, the API redirects the request, and a `POST` or `PATCH` arrives with an empty body.
</Warning>

## Step 1: Create the exam and upload the question paper

Create the exam, upload the paper, then submit it with `isFinal`. Send one PDF of up to 50 pages, or any number of `.jpg` and `.png` images, but never a PDF and images together.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + "/v1/exam/",
      headers=HEADERS,
      json={
          "examName": "Pre Board 1 - Science",
          "grade": 10,
          "subject": 1,
          "examType": 1,
          "examSet": "A",
          "examDate": "2026-03-14",
          "classSection": "A",
      },
  )
  response.raise_for_status()
  exam_id = response.json()["id"]
  exam_url = f"{BASE_URL}/v1/exam/{exam_id}/"

  with open("question-paper.pdf", "rb") as paper:
      requests.patch(
          exam_url,
          headers=HEADERS,
          files=[
              ("documents", ("paper.pdf", paper, "application/pdf"))
          ],
      ).raise_for_status()

  requests.patch(
      exam_url, headers=HEADERS, json={"isFinal": True}
  ).raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  import { openAsBlob } from "node:fs"; // Node 20 or later

  let response = await fetch(`${BASE_URL}/v1/exam/`, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({
      examName: "Pre Board 1 - Science",
      grade: 10,
      subject: 1,
      examType: 1,
      examSet: "A",
      examDate: "2026-03-14",
      classSection: "A",
    }),
  });
  if (!response.ok) throw new Error(await response.text());
  const examId = (await response.json()).id;
  const examUrl = `${BASE_URL}/v1/exam/${examId}/`;

  const paper = new FormData();
  paper.append(
    "documents",
    await openAsBlob("question-paper.pdf", {
      type: "application/pdf",
    }),
    "question-paper.pdf",
  );
  response = await fetch(examUrl, {
    method: "PATCH",
    headers: HEADERS,
    body: paper,
  });
  if (!response.ok) throw new Error(await response.text());

  response = await fetch(examUrl, {
    method: "PATCH",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ isFinal: true }),
  });
  if (!response.ok) throw new Error(await response.text());
  ```
</CodeGroup>

<Tip>
  `grade`, `subject`, and `examType` are integer codes. Get them from [Get exam evaluation constants](/api-reference/endpoint/evaluation-platform/constants). They don't change, so you can store them.
</Tip>

<Warning>
  Send `documents`, `fileUrl`, or `isFinal`, never two in one request. In a multipart request, **any** value of `isFinal` counts as sending it, including `isFinal=false`.
</Warning>

<Warning>
  The API works out a file's type from the `Content-Type` of its multipart part, not from the file name. Declare it as the samples do, or you get `400 Invalid file type`. `curl -F` fills it in for you, but `requests` and `FormData` do not.
</Warning>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "id": "d19e6868-98dd-4274-a2a5-4a616dd93ea7",
    "documents": [
      {
        "id": "0f2a7c61-9b48-4d3e-8a15-6c7d8e9f0a1b",
        "status": 1,
        "pageNo": null,
        "uploadTime": "2026-03-18T09:14:02.481293Z",
        "url": "https://<storage-host>/media/evaluation/paper.pdf",
        "fileType": 1
      }
    ]
  }
  ```

  Each file's `status` is `0` Open, `1` In Progress, `2` Completed, or `3` Failed. The exam's own `status` uses the same values, and you can read it with [Get an exam](/api-reference/endpoint/evaluation-platform/v1-exam-id-get).
</Accordion>

## Step 2: Get the extracted questions

Wait for the questions, then show them to a teacher. Until extraction finishes, this call returns `400 Exam is not processed yet`.

<CodeGroup>
  ```python Python theme={null}
  paper = wait_for(f"/v1/exam/{exam_id}/questions/")

  for section in paper["sections"]:
      print(section["sectionName"], section["totalMarks"])
  ```

  ```javascript JavaScript theme={null}
  const paperData = await waitFor(`/v1/exam/${examId}/questions/`);

  for (const section of paperData.sections) {
    console.log(section.sectionName, section.totalMarks);
  }
  ```
</CodeGroup>

<Accordion title="What you get back">
  Questions sit three levels deep: sections, then question groups, then the questions a student answers.

  ```json theme={null}
  {
    "id": "d19e6868-98dd-4274-a2a5-4a616dd93ea7",
    "examName": "Pre Board 1 - Science",
    "subjectName": "Science",
    "totalMarks": 80,
    "obtainedMarks": 0,
    "overallDuration": "180",
    "overallInstruction": "All questions are compulsory. ...",
    "sections": [
      {
        "id": "ef66afc6-d923-43c6-b2d2-e29814306214",
        "sectionName": "SECTION A",
        "totalQuestion": 20,
        "maxQuestionAttempt": 20,
        "totalMarks": 20,
        "obtainedMarks": null,
        "parentQuestions": [
          {
            "id": "fcb22595-c36a-4ddf-b9d3-00ba6d2ea466",
            "text": "Identify the option that gives the correct enzyme ...",
            "questionNumber": "Q1",
            "questionType": "MCQ",
            "isQuestionMarkMismatch": false,
            "marks": 1,
            "questions": [
              {
                "id": "226986d3-95e4-4ed7-aaa4-3e7054f9bc5b",
                "text": "(a) (i) lipase (ii) trypsin (iii) pepsin ...",
                "marks": 1.0,
                "isApproved": false,
                "isModelAnswerMarkMismatch": false,
                "supportingText": "MCQ"
              }
            ]
          }
        ]
      }
    ]
  }
  ```

  `maxQuestionAttempt` says how many items below count toward the score. Fix anything wrong now with [Edit exam questions, answers, or marks](/api-reference/endpoint/evaluation-platform/v1-generic-api): those edits lock as soon as the first answer sheet is submitted.
</Accordion>

## Step 3: Upload the model answers

Create the model answer sheet, upload the answer key, then submit it. The same file rules apply.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      f"{BASE_URL}/v1/exam/{exam_id}/model-answer/", headers=HEADERS
  )
  response.raise_for_status()
  sheet_id = response.json()["id"]
  key_url = f"{BASE_URL}/v1/exam/{exam_id}/model-answer/{sheet_id}/"

  with open("answer-key.pdf", "rb") as key_file:
      requests.patch(
          key_url,
          headers=HEADERS,
          files=[
              ("documents", ("key.pdf", key_file, "application/pdf"))
          ],
      ).raise_for_status()

  requests.patch(
      key_url, headers=HEADERS, json={"isFinal": True}
  ).raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  response = await fetch(
    `${BASE_URL}/v1/exam/${examId}/model-answer/`,
    { method: "POST", headers: HEADERS },
  );
  if (!response.ok) throw new Error(await response.text());
  const sheetId = (await response.json()).id;
  const keyUrl =
    `${BASE_URL}/v1/exam/${examId}/model-answer/${sheetId}/`;

  const key = new FormData();
  key.append(
    "documents",
    await openAsBlob("answer-key.pdf", { type: "application/pdf" }),
    "answer-key.pdf",
  );
  response = await fetch(keyUrl, {
    method: "PATCH",
    headers: HEADERS,
    body: key,
  });
  if (!response.ok) throw new Error(await response.text());

  response = await fetch(keyUrl, {
    method: "PATCH",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ isFinal: true }),
  });
  if (!response.ok) throw new Error(await response.text());
  ```
</CodeGroup>

<Note>
  An exam has one model answer sheet. Creating it twice returns the same sheet, so the call is safe to repeat.
</Note>

## Step 4: Check the model answers

Wait for the answer key, then show it to a teacher. Every question now carries a `modelAnswer` and its step marking.

<CodeGroup>
  ```python Python theme={null}
  answer_key = wait_for(
      f"/v1/exam/{exam_id}/model-answer/{sheet_id}/"
  )
  ```

  ```javascript JavaScript theme={null}
  const answerKey = await waitFor(
    `/v1/exam/${examId}/model-answer/${sheetId}/`,
  );
  ```
</CodeGroup>

<Accordion title="What you get back">
  The same paper as step 2, with a `modelAnswer` on every question:

  ```json theme={null}
  {
    "id": "226986d3-95e4-4ed7-aaa4-3e7054f9bc5b",
    "text": "(a) (i) lipase (ii) trypsin (iii) pepsin ...",
    "marks": 1.0,
    "isModelAnswerMarkMismatch": false,
    "modelAnswer": {
      "id": "57837640-c695-44a5-9667-9cfb8bafebf1",
      "answerText": "(b) (i) amylase, (ii) pepsin, (iii) trypsin",
      "stepMarking": [
        {
          "marksplit": 1.0,
          "step_wise_answer": "(b) (i) amylase, (ii) pepsin, (iii) trypsin",
          "modelanswer_diagram_description": null
        }
      ]
    }
  }
  ```

  `stepMarking` splits a question's marks across the steps of its answer, and that is what the AI grades against. When the `marksplit` values don't add up to `marks`, `isModelAnswerMarkMismatch` is `true`. Fix it before you upload any answer sheet.
</Accordion>

## Step 5: Upload a student's answer sheet

Create an answer sheet for one student, upload the pages in order, then submit it. Repeat for every student.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      f"{BASE_URL}/v1/exam/{exam_id}/answer-sheet/",
      headers=HEADERS,
      json={"studentId": "S-2043", "studentName": "Aarav Sharma"},
  )
  response.raise_for_status()
  ans_sheet_id = response.json()["id"]
  sheet_url = (
      f"{BASE_URL}/v1/exam/{exam_id}/answer-sheet/{ans_sheet_id}/"
  )

  handles = [
      open(name, "rb") for name in ["page-1.jpg", "page-2.jpg"]
  ]
  pages = [
      ("documents", (handle.name, handle, "image/jpeg"))
      for handle in handles
  ]
  try:
      requests.patch(
          sheet_url, headers=HEADERS, files=pages
      ).raise_for_status()
  finally:
      for handle in handles:
          handle.close()

  requests.patch(
      sheet_url, headers=HEADERS, json={"isFinal": True}
  ).raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  response = await fetch(
    `${BASE_URL}/v1/exam/${examId}/answer-sheet/`,
    {
      method: "POST",
      headers: { ...HEADERS, "Content-Type": "application/json" },
      body: JSON.stringify({
        studentId: "S-2043",
        studentName: "Aarav Sharma",
      }),
    },
  );
  if (!response.ok) throw new Error(await response.text());
  const ansSheetId = (await response.json()).id;
  const sheetUrl =
    `${BASE_URL}/v1/exam/${examId}/answer-sheet/${ansSheetId}/`;

  const sheet = new FormData();
  for (const name of ["page-1.jpg", "page-2.jpg"]) {
    const page = await openAsBlob(name, { type: "image/jpeg" });
    sheet.append("documents", page, name);
  }
  response = await fetch(sheetUrl, {
    method: "PATCH",
    headers: HEADERS,
    body: sheet,
  });
  if (!response.ok) throw new Error(await response.text());

  response = await fetch(sheetUrl, {
    method: "PATCH",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ isFinal: true }),
  });
  if (!response.ok) throw new Error(await response.text());
  ```
</CodeGroup>

<Tip>
  Want students to upload their own sheets? [Create an embeddable UI link](/api-reference/endpoint/evaluation-platform/embeddable-ui-link) instead. One call creates the answer sheet and a ready-made page for uploading and reading the evaluation. The new sheet's ID is the last segment of the link.
</Tip>

<Warning>
  Every uploaded file goes through a readability check, and an unreadable page is rejected with `400`. Submitting counts against a daily limit per API user and returns `429` once you reach it.
</Warning>

## Step 6: Review the scores

Wait for the marks, show them to a teacher, and apply any correction. Nothing here is visible to students yet.

<CodeGroup>
  ```python Python theme={null}
  scores = wait_for(
      f"/v1/exam/{exam_id}/answer-sheet/{ans_sheet_id}"
      "/pre-provisional-scores/"
  )
  print(scores["obtainedMarks"], "out of", scores["totalMarks"])

  group = scores["sections"][0]["parentQuestions"][0]
  answer_id = group["questions"][0]["answer"][0]["id"]

  # Optional: a teacher raises this answer's mark
  requests.patch(
      f"{BASE_URL}/v1/exam/{exam_id}/update/",
      headers=HEADERS,
      json=[
          {
              "entity": "answer",
              "id": answer_id,
              "marks": 1,
              "updateReason": "Correct enzyme named on line 2",
          }
      ],
  ).raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  const scores = await waitFor(
    `/v1/exam/${examId}/answer-sheet/${ansSheetId}` +
      "/pre-provisional-scores/",
  );
  console.log(scores.obtainedMarks, "out of", scores.totalMarks);

  const group = scores.sections[0].parentQuestions[0];
  const answerId = group.questions[0].answer[0].id;

  // Optional: a teacher raises this answer's mark
  response = await fetch(`${BASE_URL}/v1/exam/${examId}/update/`, {
    method: "PATCH",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify([
      {
        entity: "answer",
        id: answerId,
        marks: 1,
        updateReason: "Correct enzyme named on line 2",
      },
    ]),
  });
  if (!response.ok) throw new Error(await response.text());
  ```
</CodeGroup>

<Note>
  An `answer` edit that changes `text` or `marks` must include `updateReason`. It comes back as `teacherFeedback`. Editing questions and model answers is already locked at this point.
</Note>

<Accordion title="What you get back">
  Every question now has the model answer, the student's answer, and why marks were lost.

  ```json theme={null}
  {
    "id": "226986d3-95e4-4ed7-aaa4-3e7054f9bc5b",
    "text": "(a) (i) lipase (ii) trypsin (iii) pepsin ...",
    "marks": 1.0,
    "modelAnswer": { "...": "..." },
    "answer": [
      {
        "id": "f819bfd2-8aaa-42df-bd18-23cb989b2d16",
        "answerText": "(a) (i) amylase, (ii) pepsin, (iii) pepsin",
        "marks": 0.0,
        "isApproved": false,
        "diagramDescription": null,
        "aiFeedbacks": [
          {
            "feedback": "The enzyme for location (iii) is trypsin, not pepsin.",
            "student_text": "(a) (i) amylase, (ii) pepsin, (iii) pepsin",
            "justification": "Pepsin acts in the stomach, so it cannot be ...",
            "marks_deducted": "1",
            "room_for_improvement": "Revise which enzyme each organ secretes."
          }
        ],
        "teacherFeedback": null
      }
    ]
  }
  ```

  `answerText` is `No Answer` when the student left a question blank, and those answers always score `0`. Totals roll up into `obtainedMarks` on each group, each section, and the exam.
</Accordion>

## Step 7: Publish the results

Publishing is what makes scores readable. `pre-publish` releases the provisional result, which is also what lets students raise queries.

<CodeGroup>
  ```python Python theme={null}
  requests.post(
      f"{BASE_URL}/v1/exam/{exam_id}/publish-result/",
      headers=HEADERS,
      json={"entity": "pre-publish"},
  ).raise_for_status()

  response = requests.get(
      f"{BASE_URL}/v1/exam/{exam_id}/answer-sheet/{ans_sheet_id}"
      "/provisional-scores/",
      headers=HEADERS,
  )
  response.raise_for_status()
  provisional = response.json()
  ```

  ```javascript JavaScript theme={null}
  response = await fetch(
    `${BASE_URL}/v1/exam/${examId}/publish-result/`,
    {
      method: "POST",
      headers: { ...HEADERS, "Content-Type": "application/json" },
      body: JSON.stringify({ entity: "pre-publish" }),
    },
  );
  if (!response.ok) throw new Error(await response.text());

  response = await fetch(
    `${BASE_URL}/v1/exam/${examId}/answer-sheet/${ansSheetId}` +
      "/provisional-scores/",
    { headers: HEADERS },
  );
  if (!response.ok) throw new Error(await response.text());
  const provisional = await response.json();
  ```
</CodeGroup>

<Warning>
  Publishing applies to the **whole exam**, not to one answer sheet. Every student's score becomes readable at once, so review them all first.
</Warning>

When queries are settled, publish again with `{"entity": "final-publish"}` and read [Get final exam results](/api-reference/endpoint/evaluation-platform/v1-get-final-scores). That response has the same body, so it includes every mark changed since.

<Accordion title="What you get back">
  ```json theme={null}
  {
    "status": "success",
    "provisionalPublishedDate": "2026-03-19 11:04:27",
    "data": { "...": "the same scored paper as step 6" }
  }
  ```

  `provisionalPublishedDate` is UTC, written as `YYYY-MM-DD HH:MM:SS`.
</Accordion>

## Step 8: Handle student queries (optional)

A student who disagrees with a deduction raises a query against it. That sends the answer back to the AI for a second look.

<CodeGroup>
  ```python Python theme={null}
  feedback_path = (
      f"/v1/exam/{exam_id}/answer-sheet/{ans_sheet_id}"
      f"/answer/{answer_id}/feedback/"
  )
  response = requests.get(BASE_URL + feedback_path, headers=HEADERS)
  response.raise_for_status()
  feedback = response.json()["data"]

  requests.post(
      BASE_URL + feedback_path,
      headers=HEADERS,
      json={
          "feedbackId": feedback[0]["id"],
          "queryType": 2,
          "text": "I named trypsin on the second line, please check.",
      },
  ).raise_for_status()
  ```

  ```javascript JavaScript theme={null}
  const feedbackPath =
    `/v1/exam/${examId}/answer-sheet/${ansSheetId}` +
    `/answer/${answerId}/feedback/`;
  response = await fetch(BASE_URL + feedbackPath, {
    headers: HEADERS,
  });
  if (!response.ok) throw new Error(await response.text());
  const feedback = (await response.json()).data;

  response = await fetch(BASE_URL + feedbackPath, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({
      feedbackId: feedback[0].id,
      queryType: 2,
      text: "I named trypsin on the second line, please check.",
    }),
  });
  if (!response.ok) throw new Error(await response.text());
  ```
</CodeGroup>

`queryType` is `1` Classification, `2` Re-evaluation Request, or `3` Other Concerns.

<Warning>
  Every query triggers an AI re-evaluation, whatever the `queryType`. If the AI agrees with the student, the answer's marks change and the exam's totals change with them.
</Warning>

Poll the same `GET` to pick up the result. The AI's justification arrives in the query's `responses` with `createdByRole` of `AI`, and the query's `status` becomes `3` Resolved. A teacher can also [reply in the thread](/api-reference/endpoint/evaluation-platform/v1-post-feedback-query) or [close the query](/api-reference/endpoint/evaluation-platform/v1-patch-feedback-query).

## Good to know

<AccordionGroup>
  <Accordion title="Limits" icon="gauge">
    | Limit                               | Value                                                                                                                   |
    | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
    | File types                          | `.pdf`, `.jpg`, `.jpeg`, `.png`                                                                                         |
    | File size                           | 20 MB per file                                                                                                          |
    | PDF length                          | 50 pages                                                                                                                |
    | PDFs                                | One per exam, model answer sheet, or answer sheet, and never mixed with images                                          |
    | Images                              | No limit on how many. Upload them in page order.                                                                        |
    | `fileUrl`                           | One per exam, model answer sheet, or answer sheet. Public HTTPS, and the file itself must really be a PDF.              |
    | Question extraction                 | A daily limit per API user, returning `400` when reached                                                                |
    | Answer sheet evaluation             | A daily limit per API user, returning `429` when reached. Resets at 00:01 server time, and rejected requests count too. |
    | Editing questions and model answers | Locked once any answer sheet for the exam has been submitted                                                            |
    | Listing exams                       | `page_size` up to 1000, but the response's `pageSize` always reports 10                                                 |

    Both daily limits are counted per API user, which is the credential your access token comes from. Separate API users each get their own count.
  </Accordion>

  <Accordion title="Use webhooks instead of polling" icon="bell">
    Register a webhook for each step, then fetch the result when the event arrives. See [Async jobs and webhooks](/api-reference/async-jobs-and-webhooks#register-a-webhook).

    | Step                       | Event                                             |
    | -------------------------- | ------------------------------------------------- |
    | 1. Question extraction     | `exam_evaluation_question_paper_extraction`       |
    | 3. Model answer extraction | `exam_evaluation_model_answer_extraction`         |
    | 5. Answer sheet evaluation | `exam_evaluation_student_answer_sheet_evaluation` |

    Every payload carries `status` as `Completed` or `Failed`, `exam_id`, the event's `name` and `display_name`, and the `secret_hash` you registered. Model answer events add `model_answer_sheet_id`, and answer sheet events add `ans_sheet_id`. Nothing else is included, so fetch the result yourself.

    Only the answer sheet event is retried, once, after 5 seconds if your endpoint doesn't return `200` or `201`. Keep polling as a fallback.
  </Accordion>

  <Accordion title="Troubleshooting" icon="wrench">
    | Error                                                                           | Fix                                                                                                                               |
    | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
    | `Only one of documents, fileUrl, or isFinal can be processed at a time`         | Send one of the three per request. In multipart, `isFinal=false` still counts as sending it.                                      |
    | `Cannot mix PDF files with image files`                                         | Use one PDF, or only images                                                                                                       |
    | `Only one PDF file is allowed`                                                  | Merge the pages into a single PDF first                                                                                           |
    | `PDF file has 62 pages. Maximum allowed is 50 pages`                            | Split the paper across an exam and its answer key, or send images                                                                 |
    | `File size should not exceed 20 MB`                                             | Compress the scan, or send pages as separate images                                                                               |
    | `Invalid file type`                                                             | Send `.pdf`, `.jpg`, `.jpeg`, or `.png`, and declare the part's `Content-Type` as `application/pdf`, `image/jpeg`, or `image/png` |
    | `URL does not point to a valid PDF file`                                        | `fileUrl` must be public HTTPS and serve a real PDF                                                                               |
    | `Exam is not processed yet`                                                     | Question extraction hasn't finished. Wait for the exam's `status` to reach `2`.                                                   |
    | `Model Answer is not processed yet`                                             | Submit the answer key and wait before you create answer sheets                                                                    |
    | `404 No documents found for this answer sheet`                                  | You sent `isFinal` before uploading any page                                                                                      |
    | `Cannot modify question/model answer after evaluation of student answer sheets` | Fix the paper and the answer key before the first answer sheet goes in. You can still edit an `answer`.                           |
    | `Update reason is required when modifying answer text or marks`                 | Add `updateReason` to the `answer` item                                                                                           |
    | `Rate limit exceeded. You can only send N requests per day.`                    | Try again after 00:01 server time                                                                                                 |
    | `403` with `{"detail": "Token has expired"}`                                    | [Get a new access token](/api-reference/endpoint/authentication). This API answers `403`, not `401`.                              |
    | A `POST` or `PATCH` behaves as if the body were empty                           | Add the trailing slash to the path                                                                                                |
    | Still `not evaluated yet` after 10 minutes                                      | Create a new answer sheet, upload the pages again, and [contact us](https://www.crazygoldfish.com/#comp-lxumousa) with the old ID |
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Exam Evaluation API reference" icon="code" href="/api-reference/endpoint/evaluation-platform/v1-exam">
    Every field, response, and error.
  </Card>

  <Card title="How the workflow fits a school" icon="diagram-project" href="/products/evaluation-layer/workflow">
    The same flow, without the code.
  </Card>

  <Card title="Use cases" icon="lightbulb" href="/products/evaluation-layer/usecase">
    Where schools and platforms use exam evaluation.
  </Card>

  <Card title="Embeddable UI" icon="window" href="/products/evaluation-layer/embeddable-ui">
    Ready-made screens for uploading and reviewing.
  </Card>
</CardGroup>
