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

# Analyze a classroom recording with the API

> Send an audio recording of a lesson to the ClassTrack API and get rubric scores, classroom insights, an action plan, and a transcript.

ClassTrack listens to an audio recording of a lesson and reports on the teaching: a score for every criterion in your rubric, observations backed by quotes, and an action plan for the teacher. It takes two API calls.

This guide is for developers. For a non-technical overview, see [Classroom Observation features](/products/classroom-observation/features).

| Step | You call          | You get back                                                          |
| ---- | ----------------- | --------------------------------------------------------------------- |
| 1    | Start an analysis | The analysis ID                                                       |
| 2    | Get the analysis  | Rubric scores, classroom insights, an action plan, and the transcript |

<Info>
  The first call starts a background job that transcribes the recording and then scores it. How long it takes depends on the length of the recording. The `wait_for` helper below polls until the result arrives.
</Info>

## Set up

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

You also need a rubric with criteria on it, because its criteria become the categories ClassTrack scores. See [Create a rubric](/api-reference/guides/create-a-rubric).

<Warning>
  **ClassTrack takes the token without the `Bearer ` prefix.** This service reads the whole `Authorization` header as the token. Sending `Bearer YOUR_ACCESS_TOKEN` returns `401` `Unauthorized User`. Every other CrazyGoldFish API expects the prefix, so keep the two headers separate in your code.
</Warning>

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

  import requests

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


  def wait_for(analysis_id):
      """Check the analysis every 5 seconds until the result arrives."""
      path = f"/class-track/v1/analysis/{analysis_id}"
      for _ in range(240):  # Give up after 20 minutes
          response = requests.get(BASE_URL + path, headers=HEADERS)
          response.raise_for_status()
          body = response.json()
          if "data" in body:  # Finished
              return body["data"]
          message = body["detail"][0]["msg"]
          if message == "Analysis is Failed":
              raise RuntimeError(message)
          time.sleep(5)
      raise TimeoutError(f"{analysis_id} took longer than 20 minutes")
  ```

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

  // Check the analysis every 5 seconds until the result arrives
  async function waitFor(analysisId) {
    const path = `/class-track/v1/analysis/${analysisId}`;
    for (let i = 0; i < 240; i++) { // Give up after 20 minutes
      const response = await fetch(BASE_URL + path, { headers: HEADERS });
      if (!response.ok) throw new Error(await response.text());
      const body = await response.json();
      if (body.data) return body.data; // Finished
      const message = body.detail[0].msg;
      if (message === "Analysis is Failed") throw new Error(message);
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
    throw new Error(`${analysisId} took longer than 20 minutes`);
  }
  ```
</CodeGroup>

<Warning>
  The GET endpoint returns `200` while the analysis is still running, and `200` again when it has finished. The helper above branches on the body, not the status code.
</Warning>

## Step 1: Start the analysis

Upload the recording to storage of your own first, then send its link with the class details. The link must start with `https://`, and its path must end with `.mp3`.

<CodeGroup>
  ```python Python theme={null}
  response = requests.post(
      BASE_URL + "/class-track/v1/analysis",
      headers=HEADERS,
      json={
          "teacher_id": "T-2087",
          "institution_id": "INST-14",
          "board_id": BOARD_ID,
          "grade_id": GRADE_ID,
          "subject_id": SUBJECT_ID,
          "rubric_id": RUBRIC_ID,
          "date": "2026-09-16",
          "link": "https://storage.example.com/lesson.mp3",
      },
  )
  response.raise_for_status()
  analysis_id = response.json()["detail"][0]["id"]
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${BASE_URL}/class-track/v1/analysis`, {
    method: "POST",
    headers: { ...HEADERS, "Content-Type": "application/json" },
    body: JSON.stringify({
      teacher_id: "T-2087",
      institution_id: "INST-14",
      board_id: BOARD_ID,
      grade_id: GRADE_ID,
      subject_id: SUBJECT_ID,
      rubric_id: RUBRIC_ID,
      date: "2026-09-16",
      link: "https://storage.example.com/lesson.mp3",
    }),
  });
  if (!response.ok) throw new Error(await response.text());
  const body = await response.json();
  const analysisId = body.detail[0].id;
  ```
</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). `RUBRIC_ID` is a rubric UUID from [List rubrics](/api-reference/endpoint/rubrics/v1-rubric-list), or its integer ID.
</Tip>

<Accordion title="What you get back">
  ```json theme={null}
  {
    "detail": [
      {
        "id": "7f3c9a24-5b18-4d6e-9c07-2a4f8e1b6d35",
        "message": "Class track analysis process started!"
      }
    ]
  }
  ```

  The status code is `200`, not `201`, and the field is `message`, not `msg`. Save `id`: it is the only way to read the result.
</Accordion>

## Step 2: Get the analysis

Poll the analysis ID until the result arrives.

<CodeGroup>
  ```python Python theme={null}
  analysis = wait_for(analysis_id)
  result = analysis["result"]

  print(result["overall_teaching_score"])
  for item in result["rubric_evaluation"]:
      print(item["category"], item["score"])
      print(item["recommendations"])
  ```

  ```javascript JavaScript theme={null}
  const analysis = await waitFor(analysisId);
  const result = analysis.result;

  console.log(result.overall_teaching_score);
  for (const item of result.rubric_evaluation) {
    console.log(item.category, item.score);
    console.log(item.recommendations);
  }
  ```
</CodeGroup>

<Accordion title="What you get back">
  Trimmed to one rubric criterion and one insight. A real response has one `rubric_evaluation` entry per criterion in your rubric.

  ```json theme={null}
  {
    "data": {
      "id": "7f3c9a24-5b18-4d6e-9c07-2a4f8e1b6d35",
      "result": {
        "overall_teaching_score": "Proficient",
        "summary": "A clear recap opened the lesson, but few students spoke.",
        "rubric_evaluation": [
          {
            "category": "Student participation",
            "score": "Developing",
            "observations": "Questions went to volunteers, so few took part.",
            "recommendations": "Use a no-hands-up round twice a lesson.",
            "examples": ["anyone else who wants to try this one"]
          }
        ],
        "classroom_insights": [
          {
            "area": "Instructional Clarity",
            "observation": "Activity instructions were given once, then repeated.",
            "improvement_strategy": "Give instructions in numbered steps.",
            "examples": ["work with the person next to you"]
          }
        ],
        "action_plan": {
          "immediate_actions": ["Put the key terms on the board."],
          "short_term_goals": ["Check understanding every five minutes."],
          "long_term_development": ["Build a bank of questioning strategies."]
        }
      },
      "extracted_transcript": "good morning last week we looked at magnets"
    }
  }
  ```

  `extracted_transcript` is the recognized speech as one string, in lexical form, so it has no punctuation or capitalization.
</Accordion>

## Good to know

<AccordionGroup>
  <Accordion title="Limits" icon="gauge">
    | Limit                          | Value                                                                                                                                 |
    | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
    | Recording                      | One audio file per analysis, linked with `https://`, path ending in lower-case `.mp3`                                                 |
    | Where it is hosted             | Anywhere public. `localhost` and private IP ranges are rejected, and so is a query string after the file name.                        |
    | Reachability                   | The file must answer an HTTP `HEAD` request with `200`, `206`, or `302` within 10 seconds, and stay reachable until the job finishes. |
    | Language                       | The recording is transcribed as English, and the transcription service also recognizes Hindi.                                         |
    | `teacher_id`, `institution_id` | Your own identifiers, 50 characters or fewer                                                                                          |
    | `date`                         | The day of the lesson. Today is allowed, a future date is rejected.                                                                   |
    | Rubric                         | Must already exist, with criteria on it. The criteria become the scored categories.                                                   |
  </Accordion>

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

    ```json Payload theme={null}
    {
      "id": "7f3c9a24-5b18-4d6e-9c07-2a4f8e1b6d35",
      "name": "class_track_analysis",
      "display_name": "Class Track Analysis",
      "secret_hash": "a-long-random-string-you-generate",
      "status": "Completed"
    }
    ```

    `id` is the analysis ID, and `status` is `Completed` or `Failed`. The event only goes to the API user who started the analysis, so register the webhook with the same user.

    **Keep polling as a fallback.** If the recording can't be transcribed, the analysis is marked `Failed` without the event being sent.
  </Accordion>

  <Accordion title="Troubleshooting" icon="wrench">
    | Error                                                                           | Fix                                                                                                      |
    | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
    | `401` `Unauthorized User` with a token that works elsewhere                     | Drop the `Bearer ` prefix from the `Authorization` header                                                |
    | `422` `Invalid URL. Only public HTTPS URLs pointing to .mp3 files are allowed.` | The link is private, expired, redirects somewhere unreachable, or took more than 10 seconds to answer    |
    | `422` `Value error, Link must start with https://`                              | Use an HTTPS link                                                                                        |
    | `422` `Value error, Allowed file types are .mp3`                                | The path must end in lower-case `.mp3`, with no query string after it                                    |
    | `422` `Value error, Date cannot be greater than today`                          | Send the day the lesson happened                                                                         |
    | `422` `Value error, Rubric with UUID ... does not exist`                        | Use a rubric ID from [List rubrics](/api-reference/endpoint/rubrics/v1-rubric-list)                      |
    | `404` `Analysis with id ... not found`                                          | Check the ID from step 1                                                                                 |
    | The submission fails                                                            | Check that `teacher_id` and `institution_id` are 50 characters or fewer, then retry                      |
    | `Analysis is Failed`                                                            | The recording could not be transcribed or scored. Check the link still works, then start a new analysis. |
    | Still `In progress` after a long wait                                           | [Contact us](https://www.crazygoldfish.com/#comp-lxumousa) with the analysis ID                          |
  </Accordion>
</AccordionGroup>

## Related

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

  <Card title="Classroom Observation" icon="microphone" href="/products/classroom-observation/features">
    What teachers and coaches see, without the code.
  </Card>
</CardGroup>
