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

# Build a lesson plan with the API

> Turn a topic, textbook photos, or a recording into a full lesson plan with the Lesson Plan Builder API, with a teacher review step between calls.

The Lesson Plan Builder writes a class-ready lesson plan for a board, grade, subject, and topic: what to cover, what students should be able to do, which materials to use, and a minute-by-minute script for each stage of the period. It takes four calls, and a teacher can review and edit between each one.

This guide is for developers. For a non-technical overview, see [Lesson Plan Builder features](/products/lesson-plan/features).

| Step | You call                         | You get back                                              | Teacher can                           |
| ---- | -------------------------------- | --------------------------------------------------------- | ------------------------------------- |
| 1    | Start a lesson plan              | Topics, concepts, prerequisites, outcomes, and objectives | Add or remove items                   |
| 2    | Finalize metadata                | Materials, teaching strategies, and a six-stage timeline  | Pick options and change stage minutes |
| 3    | Build lesson content             | A teaching script for every stage                         | Use the lesson plan                   |
| 4    | Add enrichment blocks (optional) | Extra sections, such as differentiation notes             |                                       |

<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 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:
              data = body["data"]
              if data["status"] == "Completed":
                  return data
              if data["status"] == "Failed":
                  raise RuntimeError(f"{path} failed")
          elif response.status_code in (400, 404):
              first = body["detail"][0]
              if first.get("status") == "Failed":
                  raise RuntimeError(first["msg"])
          else:
              response.raise_for_status()
          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) {
        const data = body.data;
        if (data.status === "Completed") return data;
        if (data.status === "Failed") throw new Error(`${path} failed`);
      } else if ([400, 404].includes(response.status)) {
        const first = body.detail[0];
        if (first.status === "Failed") throw new Error(first.msg);
      } else {
        throw new Error(JSON.stringify(body));
      }
      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 returns `404` with a `step not found` message. A failed job returns `400` with `"status": "Failed"`, except [Get lesson plan content](/api-reference/endpoint/lesson-plan/v1-instructional-framework-builder-get) which returns `200` with `"status": "Failed"`. `wait_for` handles all three.
</Note>

## Step 1: Start the lesson plan

Send the board, grade, and subject, how long the period is, and one source: a topic, up to four images, or one audio recording. The fields go in a form, not in JSON.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + "/lesson-plan/v1/metadata",
      headers=HEADERS,
      data={
          "board": BOARD_ID,
          "grade": GRADE_ID,
          "subject": SUBJECT_ID,
          "section": "A",
          "duration_minutes": 45,
          "topic": "Magnetic Effects of Electric Current",
      },
  )
  response.raise_for_status()
  lesson_plan_id = response.json()["detail"][0]["id"]

  metadata = wait_for(f"/lesson-plan/v1/metadata/{lesson_plan_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("section", "A");
  form.append("duration_minutes", "45");
  form.append("topic", "Magnetic Effects of Electric Current");

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

  const metadata = await waitFor(
    `/lesson-plan/v1/metadata/${lessonPlanId}`,
  );
  ```
</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>

<Warning>
  `duration_minutes` must be between 30 and 90. Send exactly one of `topic`, `documents`, or `audio`.
</Warning>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "id": "7d2f1b90-4c6a-4f3e-b1d7-9a5e2c8f0b34",
    "status": "Completed",
    "duration_minutes": 45,
    "topic": "Magnetic Effects of Electric Current",
    "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": { "...": "..." } },
      "competencies": { "competency_list": [{ "...": "..." }] }
    }
  }
  ```

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

## Step 2: Finalize the metadata

Send `subject_matter` and `learning_standards` back, with any edits. You get the materials, the teaching strategies, and the six stages of the lesson with minutes for each.

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

  aligned = wait_for(
      f"/lesson-plan/v1/finalize-metadata/{lesson_plan_id}"
  )
  alignment = aligned["alignment"]
  ```

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

  const aligned = await waitFor(
    `/lesson-plan/v1/finalize-metadata/${lessonPlanId}`,
  );
  const alignment = aligned.alignment;
  ```
</CodeGroup>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "status": "Completed",
    "alignment": {
      "materials_options": [
        {
          "ai_recommendations": [
            {
              "name": "Bar magnets",
              "description": "Two per group of four students"
            },
            {
              "name": "Iron filings and paper",
              "description": "To make the field pattern visible"
            }
          ],
          "additional_recommendations": []
        }
      ],
      "instructional_strategies_options": [{ "...": "..." }],
      "instructional_blocks": {
        "introduction": {
          "component_options": [
            {
              "name": "Magnet hook",
              "description": "Ask why a compass needle moves"
            }
          ],
          "formative_questions": ["What do you know about magnets?"],
          "duration": 5
        },
        "development": { "...": "...", "duration": 15 },
        "guided_practice": { "...": "...", "duration": 10 },
        "independent_practice": { "...": "...", "duration": 5 },
        "closure": { "...": "...", "duration": 5 },
        "summative_assessment": { "...": "...", "duration": 5 }
      }
    }
  }
  ```

  The API adjusts the six `duration` values so they add up to `duration_minutes`, aiming to keep each stage at 5 minutes or more. The 5 minute floor is a target, not a rule the API enforces, so read the values rather than assuming a minimum.
</Accordion>

## Step 3: Build the lesson content

Send the alignment back, wrapped in an `alignment` key. Keep only the options the teacher chose. You get a script for every stage.

<Warning>
  The six `duration` values must still add up to `duration_minutes`. If the teacher adds 5 minutes to one stage, take 5 off another.
</Warning>

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL
      + f"/lesson-plan/v1/instructional-framework-builder/{lesson_plan_id}",
      headers=HEADERS,
      json={"alignment": alignment},
  )
  response.raise_for_status()

  content = wait_for(
      f"/lesson-plan/v1/instructional-framework-builder/{lesson_plan_id}"
  )
  blocks = content["content_generation"]
  ```

  ```javascript JavaScript theme={null}
  const path =
    `/lesson-plan/v1/instructional-framework-builder/${lessonPlanId}`;
  const buildResponse = await fetch(BASE_URL + path, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({ alignment }),
  });
  if (!buildResponse.ok) throw new Error(await buildResponse.text());

  const content = await waitFor(path);
  const blocks = content.content_generation;
  ```
</CodeGroup>

<Accordion title="What you get back">
  Each stage has a `components` list. The `summative_assessment` stage comes back as `assessment_block`.

  ```json theme={null}
  {
    "status": "Completed",
    "content_generation": {
      "introduction_block": {
        "components": [
          {
            "component_name": "Magnet hook",
            "duration_minutes": 5,
            "materials_used": ["Bar magnets", "Plotting compass"],
            "implementation_script": "Hold a compass near a wire carrying current and ask the class why the needle turns.",
            "formative_questions": ["Why does the needle move?"],
            "expected_responses": ["The current affects the space around the wire."],
            "teacher_notes": "Keep the guesses on the board for the closure."
          }
        ]
      },
      "development_block": { "components": [{ "...": "..." }] },
      "guided_practice_block": { "components": [{ "...": "..." }] },
      "independent_practice_block": { "components": [{ "...": "..." }] },
      "closure_block": { "components": [{ "...": "..." }] },
      "assessment_block": { "components": [{ "...": "..." }] }
    }
  }
  ```

  The stages are written in parallel, so they appear one at a time while `status` is still `In Progress`.
</Accordion>

## Step 4: Add enrichment blocks (optional)

Add extra sections to the finished plan, such as differentiation notes or real-life connections. Ask for all the blocks you want in one call: this step runs only once.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL
      + f"/lesson-plan/v1/additional-enrichment-block/{lesson_plan_id}",
      headers=HEADERS,
      json={
          "additional_blocks": [
              "differentiation",
              "real_life_connections",
          ],
          "duration_minutes": 45,
      },
  )
  response.raise_for_status()

  enriched = wait_for(
      f"/lesson-plan/v1/additional-enrichment-block/{lesson_plan_id}"
  )
  ```

  ```javascript JavaScript theme={null}
  const blockPath =
    `/lesson-plan/v1/additional-enrichment-block/${lessonPlanId}`;
  const enrichResponse = await fetch(BASE_URL + blockPath, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({
      additional_blocks: ["differentiation", "real_life_connections"],
      duration_minutes: 45,
    }),
  });
  if (!enrichResponse.ok) throw new Error(await enrichResponse.text());

  const enriched = await waitFor(blockPath);
  ```
</CodeGroup>

<Tip>
  The six block keys come from [List enrichment block options](/api-reference/endpoint/lesson-plan/v1-additional-enrichment-blocks-list).
</Tip>

<Accordion title="What you get back">
  The response repeats the whole lesson plan and adds `additional_blocks`.

  ```json theme={null}
  {
    "status": "Completed",
    "additional_blocks": [
      {
        "name": "Differentiation",
        "description": "Give students who need support a pre-drawn magnet outline to trace field lines onto. Ask students who finish early to predict the field of two solenoids side by side."
      },
      {
        "name": "Real Life Connections",
        "description": "Link the field pattern to how a loudspeaker moves its cone and to the magnetic strip on a metro card."
      }
    ]
  }
  ```
</Accordion>

## Good to know

<AccordionGroup>
  <Accordion title="Limits" icon="gauge">
    | Limit                 | Value                                                                                                                                      |
    | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
    | Lesson length         | 30 to 90 minutes                                                                                                                           |
    | Source material       | One of `topic`, `documents` (up to 4 `.jpg`, `.jpeg`, or `.png` images, 5 MB each), or `audio` (one `.mp3`, 5 MB)                          |
    | Stage durations       | The API adjusts them so the six add up to `duration_minutes`. It aims to keep every stage at 5 minutes or more, but does not guarantee it. |
    | Enrichment blocks     | Six fixed options, all generated in one call                                                                                               |
    | Starting lesson plans | Daily limit per account. Requests rejected with `400` count too.                                                                           |
    | Each step             | Runs once per lesson plan. 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 lesson plan   | `lesson_plan_initialize_metadata`             |
    | 2. Finalize metadata     | `lesson_plan_finalize_metadata`               |
    | 3. Build lesson content  | `lesson_plan_instructional_framework_builder` |
    | 4. Add enrichment blocks | `lesson_plan_additional_enrichment_blocks`    |

    Every event fires for both outcomes, so read `status` in the payload: `Completed` or `Failed`.
  </Accordion>

  <Accordion title="Troubleshooting" icon="wrench">
    | Error                                                                               | Fix                                                                                                            |
    | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
    | `Either topic, documents, or audio must be provided.`                               | Send one source                                                                                                |
    | `Only one of topic, documents, or audio should be provided.`                        | Send only one source                                                                                           |
    | `Duration minutes must be at least 30 minutes.`                                     | Use 30 to 90 minutes                                                                                           |
    | `Sum of all block durations 50 minutes must equal lesson plan duration 45 minutes.` | Adjust the stage durations so they add up                                                                      |
    | `Lesson plan metadata is not finalized!`                                            | Wait for step 1 to reach `Completed`                                                                           |
    | `Lesson plan instructional framework builder is not finalized!`                     | Wait for step 3 to reach `Completed`                                                                           |
    | `... is already In Progress!` or `already Completed!`                               | The step already ran. Get its result instead.                                                                  |
    | Job failed with `No content retrieved`                                              | There's no indexed textbook content for that board, grade, and subject. Send `documents` instead of `topic`.   |
    | `Daily rate limit exceeded`                                                         | Try again after 00:01 server time                                                                              |
    | Still `In Progress` after 10 minutes                                                | Start a new lesson plan and [contact us](https://www.crazygoldfish.com/#comp-lxumousa) with the lesson plan ID |
  </Accordion>
</AccordionGroup>

## Related

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

  <Card title="Lesson Plan Builder features" icon="sparkles" href="/products/lesson-plan/features">
    What the product does, without the code.
  </Card>

  <Card title="How the workflow runs" icon="diagram-project" href="/products/lesson-plan/workflow">
    The same four steps, from a teacher's side.
  </Card>

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

Looking for classroom examples? See [Lesson Plan Builder use cases](/products/lesson-plan/usecases).
