> ## 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.

# Generate a worksheet with the API

> Turn a topic into a worksheet with questions, an answer key, and explanations using the Worksheet Builder API, with a teacher review step between calls.

The Worksheet Builder writes practice questions, answers, and explanations for a class, subject, and topic. It takes four API calls, and a teacher can review the draft after each one.

This guide is for developers. For a non-technical overview, see [Worksheet Builder features](/products/worksheet/features).

| Step | You call                         | You get back                              | Teacher can                                 |
| ---- | -------------------------------- | ----------------------------------------- | ------------------------------------------- |
| 1    | Start a worksheet                | Topics, concepts, and learning objectives | Add or remove topics                        |
| 2    | Configure questions              | A plan for every question                 | Change types, difficulty, or Bloom's levels |
| 3    | Generate questions               | Questions and an answer key               | Use the worksheet                           |
| 4    | Generate explanations (optional) | An explanation for every question         |                                             |

<Info>
  Each call starts a background job. Wait until the job's status is `Completed` before the next call. 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 json
  import time

  import requests

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


  def wait_for(path):
      """Check the job every 5 seconds until it's Completed."""
      for _ in range(120):  # Give up after 10 minutes
          response = requests.get(BASE_URL + path, headers=HEADERS)
          body = response.json()
          if response.status_code == 200:
              if body["data"]["status"] == "Completed":
                  return body["data"]
          elif response.status_code not in (400, 404):
              response.raise_for_status()
          elif body["detail"][0].get("status") == "Failed":
              raise RuntimeError(body["detail"][0]["msg"])
          time.sleep(5)
      raise TimeoutError(f"{path} took longer than 10 minutes")
  ```

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

  // Check the job every 5 seconds until it's Completed
  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 });
      const body = await response.json();
      if (response.status === 200) {
        if (body.data.status === "Completed") return body.data;
      } else if (![400, 404].includes(response.status)) {
        throw new Error(JSON.stringify(body));
      } else if (body.detail[0].status === "Failed") {
        throw new Error(body.detail[0].msg);
      }
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
    throw new Error(`${path} took longer than 10 minutes`);
  }
  ```
</CodeGroup>

<Note>
  While a job is starting, the API can briefly return `404`, and explanations return `400` until they're ready. `wait_for` keeps checking in both cases and stops only if the job failed.
</Note>

## Step 1: Start the worksheet

Send the board, grade, and subject, a topic, and how many questions you want. The three distributions are JSON strings.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + "/worksheet/v1/metadata",
      headers=HEADERS,
      data={
          "board": BOARD_ID,
          "grade": GRADE_ID,
          "subject": SUBJECT_ID,
          "topic": "Magnetic Effects of Electric Current",
          "number_of_questions": 5,
          "question_distribution": json.dumps(
              {"mcq_single_answer": 3, "short_answer": 2}
          ),
          "difficulty_level_distribution": json.dumps(
              {"easy": 40, "medium": 40, "hard": 20}
          ),
          "bloom_taxonomy_distribution": json.dumps(
              {"remember": 40, "understand": 40, "apply": 20}
          ),
      },
  )
  response.raise_for_status()
  worksheet_id = response.json()["detail"][0]["id"]

  metadata = wait_for(f"/worksheet/v1/metadata/{worksheet_id}")
  ```

  ```javascript JavaScript theme={null}
  const form = new FormData();
  form.append("board", BOARD_ID);
  form.append("grade", GRADE_ID);
  form.append("subject", SUBJECT_ID);
  form.append("topic", "Magnetic Effects of Electric Current");
  form.append("number_of_questions", "5");
  form.append("question_distribution",
    JSON.stringify({ mcq_single_answer: 3, short_answer: 2 }));
  form.append("difficulty_level_distribution",
    JSON.stringify({ easy: 40, medium: 40, hard: 20 }));
  form.append("bloom_taxonomy_distribution",
    JSON.stringify({ remember: 40, understand: 40, apply: 20 }));

  const response = await fetch(BASE_URL + "/worksheet/v1/metadata", {
    method: "POST",
    headers: HEADERS,
    body: form,
  });
  if (!response.ok) throw new Error(await response.text());
  const worksheetId = (await response.json()).detail[0].id;

  const metadata = await waitFor(`/worksheet/v1/metadata/${worksheetId}`);
  ```
</CodeGroup>

<Tip>
  Get `BOARD_ID`, `GRADE_ID`, and `SUBJECT_ID` from [List boards](/api-reference/endpoint/metadata/boards), [List grades](/api-reference/endpoint/metadata/grades), and [List subjects](/api-reference/endpoint/metadata/subjects). They don't change, so you can store them.
</Tip>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "id": "152764ca-1685-427c-aba7-4b92dcc0d60f",
    "status": "Completed",
    "subject_matter": {
      "topics_to_be_covered": {
        "topic": {
          "ai_recommendations": [
            "Magnetic field and field lines around a bar magnet",
            "Right-hand thumb rule"
          ],
          "additional_recommendations": []
        }
      },
      "key_concepts_and_subconcepts": { "concepts": { "...": "..." } },
      "prerequisite_knowledge": { "prerequisite": { "...": "..." } }
    },
    "learning_standards": {
      "learning_outcomes": { "outcomes": { "...": "..." } },
      "smart_learning_objectives": { "objects": { "...": "..." } }
    }
  }
  ```

  A teacher can remove items, or add their own to `additional_recommendations`.
</Accordion>

## Step 2: Configure the questions

Send `subject_matter` and `learning_standards` back, with any edits. You get a plan for every question.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + f"/worksheet/v1/question-config/{worksheet_id}",
      headers=HEADERS,
      json={
          "subject_matter": metadata["subject_matter"],
          "learning_standards": metadata["learning_standards"],
      },
  )
  response.raise_for_status()

  config = wait_for(f"/worksheet/v1/question-config/{worksheet_id}")
  plan = config["question_configuration"]
  ```

  ```javascript JavaScript theme={null}
  const configResponse = await fetch(
    `${BASE_URL}/worksheet/v1/question-config/${worksheetId}`,
    {
      method: "POST",
      headers: { ...HEADERS, "Content-Type": "application/json" },
      body: JSON.stringify({
        subject_matter: metadata.subject_matter,
        learning_standards: metadata.learning_standards,
      }),
    },
  );
  if (!configResponse.ok) throw new Error(await configResponse.text());

  const config = await waitFor(`/worksheet/v1/question-config/${worksheetId}`);
  const plan = config.question_configuration;
  ```
</CodeGroup>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "question_type_summary": [
      { "question_type": "mcq_single_answer", "count": 3 },
      { "question_type": "short_answer", "count": 2 }
    ],
    "bloom_summary": [
      { "bloom": "remember", "question_count": 2 },
      { "bloom": "understand", "question_count": 2 },
      { "bloom": "apply", "question_count": 1 }
    ],
    "difficulty_summary": [
      { "difficulty": "easy", "question_count": 2 },
      { "difficulty": "medium", "question_count": 2 },
      { "difficulty": "hard", "question_count": 1 }
    ],
    "question_summary": [
      {
        "question_type": "mcq_single_answer",
        "learning_objectives": "Recall the properties of magnetic field lines",
        "bloom": "remember",
        "difficulty": "easy",
        "question_count": 2
      }
    ]
  }
  ```

  `question_summary` is trimmed here. In a real response, its counts add up to the number of questions.
</Accordion>

## Step 3: Generate the questions

Send the plan back, with any edits. You get the questions and the answer key.

<Warning>
  If you edit the plan, the counts in **each** of the four lists must still add up to `number_of_questions`.
</Warning>

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + f"/worksheet/v1/generate-worksheet/{worksheet_id}",
      headers=HEADERS,
      json=plan,
  )
  response.raise_for_status()

  worksheet = wait_for(f"/worksheet/v1/generate-worksheet/{worksheet_id}")
  questions = worksheet["questions"]
  ```

  ```javascript JavaScript theme={null}
  const generateResponse = await fetch(
    `${BASE_URL}/worksheet/v1/generate-worksheet/${worksheetId}`,
    {
      method: "POST",
      headers: { ...HEADERS, "Content-Type": "application/json" },
      body: JSON.stringify(plan),
    },
  );
  if (!generateResponse.ok) throw new Error(await generateResponse.text());

  const worksheet = await waitFor(`/worksheet/v1/generate-worksheet/${worksheetId}`);
  const questions = worksheet.questions;
  ```
</CodeGroup>

<Accordion title="What you get back">
  Questions are grouped by type. Only multiple-choice questions have `options`.

  ```json theme={null}
  {
    "mcq_single_answer": [
      {
        "question": "Which statement about magnetic field lines is correct?",
        "options": [
          "A. They cross each other near the poles",
          "B. They start at the south pole outside the magnet",
          "C. They are closer together where the field is stronger",
          "D. They are straight lines around a bar magnet"
        ],
        "answer": "C",
        "tags": { "bloom": "Remember", "difficulty": "Easy" }
      }
    ],
    "short_answer": [
      {
        "question": "Why do two magnetic field lines never intersect?",
        "answer": "A compass needle can't point in two directions at once.",
        "tags": { "bloom": "Understand", "difficulty": "Medium" }
      }
    ]
  }
  ```
</Accordion>

## Step 4: Add explanations (optional)

Add a step-by-step explanation, key concepts, and common mistakes to every question.

<CodeGroup>
  ```python Python theme={null}
  requests.post(
      BASE_URL + f"/worksheet/v1/generate-explanations/{worksheet_id}",
      headers=HEADERS,
  ).raise_for_status()

  explained = wait_for(f"/worksheet/v1/generate-explanations/{worksheet_id}")
  ```

  ```javascript JavaScript theme={null}
  const explainResponse = await fetch(
    `${BASE_URL}/worksheet/v1/generate-explanations/${worksheetId}`,
    { method: "POST", headers: HEADERS },
  );
  if (!explainResponse.ok) throw new Error(await explainResponse.text());

  const explained = await waitFor(`/worksheet/v1/generate-explanations/${worksheetId}`);
  ```
</CodeGroup>

Each question in `explained["questions"]` now has an `explanations` list.

## Good to know

<AccordionGroup>
  <Accordion title="Limits" icon="gauge">
    | Limit                   | Value                                                                                                   |
    | ----------------------- | ------------------------------------------------------------------------------------------------------- |
    | Questions per worksheet | Up to 10                                                                                                |
    | Source material         | One of `topic`, `documents` (up to 4 `.jpg` or `.png` images, 5 MB each), or `audio` (one `.mp3`, 5 MB) |
    | Distributions           | Question counts add up to `number_of_questions`. Difficulty and Bloom's percentages add up to 100.      |
    | Starting worksheets     | Daily limit per account. Rejected requests count too.                                                   |
    | Each step               | Runs once per worksheet. You can retry a step only if it failed.                                        |
  </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. Start a worksheet     | `worksheet_initialize_metadata`    |
    | 2. Configure questions   | `worksheet_question_configuration` |
    | 3. Generate questions    | `worksheet_generation`             |
    | 4. Generate explanations | `worksheet_explanation_generation` |
  </Accordion>

  <Accordion title="Troubleshooting" icon="wrench">
    | Error                                                             | Fix                                                                                                        |
    | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
    | `Total questions in distribution must equal number_of_questions.` | Make the question counts add up to `number_of_questions`                                                   |
    | `Total mismatch: total_questions are 5 but ...`                   | Fix the list named in the message so it adds up                                                            |
    | `Only one of topic, documents, or audio should be provided.`      | Send only one source                                                                                       |
    | `... already In Progress.` or `already Completed.`                | The step already ran. Get its result instead.                                                              |
    | Job failed with `No content found`                                | There's no textbook content for that board, grade, and subject. Send `documents` instead of `topic`.       |
    | `429 Daily rate limit exceeded`                                   | Try again after 00:01 server time                                                                          |
    | Still `In Progress` after 10 minutes                              | Start a new worksheet and [contact us](https://www.crazygoldfish.com/#comp-lxumousa) with the worksheet ID |
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Worksheet API reference" icon="code" href="/api-reference/endpoint/worksheet/v1-worksheet-meta-data-post">
    Every field, response, and error.
  </Card>

  <Card title="List worksheets" icon="list" href="/api-reference/endpoint/worksheet/v1-worksheet-listing">
    Show a teacher's worksheet history.
  </Card>
</CardGroup>
