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

# Async jobs and webhooks

> How CrazyGoldFish runs AI work as background jobs, how to poll for results, and how to register webhooks for job completion events.

Grading an answer sheet or writing a worksheet takes the AI longer than a normal web request allows. So the API accepts the work, gives you a job ID, and finishes it in the background. It works like ordering at a counter: you get an order number and collect your order when it's ready.

## How does a background job work?

1. A `POST` or `PATCH` request starts the job and returns right away with an ID.
2. The job runs in the background.
3. You find out it finished in one of two ways:
   * **Polling:** your server checks the job's status every few seconds. See [Poll for results](#poll-for-results).
   * **Webhooks:** CrazyGoldFish calls your server as soon as the job finishes. See [Receive webhooks](#receive-webhooks).
4. You fetch the result with a `GET` request.

## Job status values

| API family                             | Field              | Values                                               |
| -------------------------------------- | ------------------ | ---------------------------------------------------- |
| Lesson Plan, Worksheet                 | `status` (string)  | `In Progress`, `Completed`, `Failed`                 |
| Action Plans                           | `status` (string)  | `In Progress`, `Completed`, `Partial`, `Failed`      |
| ClassTrack                             | `status` (string)  | `In progress`, `Completed`, `Failed`                 |
| Exam Evaluation, Assignment Evaluation | `status` (integer) | `0` Open, `1` In Progress, `2` Completed, `3` Failed |

`Partial` means the plan was built, but some questions failed. Treat it as a result worth showing, not an error.

## Poll for results

* **Poll every 5 seconds.** Faster polling doesn't make jobs finish sooner.
* **Expect a short `404` at the start.** Right after you start a Lesson Plan or Worksheet job, its `GET` endpoint returns `404` with a `step not found` message until a worker picks the job up. Keep polling.
* **A failed job usually returns `400`,** with `"status": "Failed"` and a `msg` explaining why. Action Plans, ClassTrack, and [Get lesson plan content](/api-reference/endpoint/lesson-plan/v1-instructional-framework-builder-get) return `200` with `"status": "Failed"` instead, so check `status` on every `200` too.
* **Set a timeout.** Some failures leave a job `In Progress`. If a job hasn't completed after 10 minutes, stop polling, start a new job, and [contact CrazyGoldFish](https://www.crazygoldfish.com/#comp-lxumousa) with the job ID.

Each product guide includes a ready-to-use polling helper. For example, see [Generate a worksheet](/api-reference/guides/generate-a-worksheet#set-up).

## Receive webhooks

A webhook is a message CrazyGoldFish sends to your server when a job completes or fails, so you don't need to keep checking. Technically, it's a `POST` request to an HTTPS address you register.

### Register a webhook

<Steps>
  <Step title="Find the event's template ID">
    ```bash theme={null}
    curl https://api.crazygoldfish.com/webhooks/v1/webhook-api-templates/ \
      -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
    ```

    ```json Response theme={null}
    [
      {
        "id": "2820c61f-64f8-4a24-9276-6ef855864772",
        "name": "worksheet_generation",
        "display_name": "Worksheet Generation",
        "description": null
      }
    ]
    ```

    The real response lists every event. See [List webhook event templates](/api-reference/endpoint/webhook/v1-get-webhook-template).
  </Step>

  <Step title="Register your endpoint">
    Send the template `id`, your HTTPS URL, and a secret that you generate:

    ```bash theme={null}
    curl -X POST https://api.crazygoldfish.com/webhooks/v1/webhook/ \
      -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "api_template": "2820c61f-64f8-4a24-9276-6ef855864772",
        "endpoint_url": "https://example.com/webhooks/crazygoldfish",
        "secret_hash": "a-long-random-string-you-generate"
      }'
    ```

    Register one webhook for each event you want.
  </Step>
</Steps>

<Warning>
  **Register webhooks with the same API user that starts the jobs.** A webhook only fires for jobs started by the API user who registered it. Webhooks registered by a different user are ignored.
</Warning>

Other rules:

* `endpoint_url` must start with `https://`. A successful registration returns `201`.
* Each API user can register one webhook per event. A second one returns `400` `Webhook already exists`.
* To change the URL, send `PATCH /webhooks/v1/webhook/{id}/`. It requires `endpoint_url` and replaces the stored one, and **if you leave out `secret_hash`, the saved secret is removed.** Send both fields every time.
* There is no endpoint to delete a webhook. [Contact CrazyGoldFish](https://www.crazygoldfish.com/#comp-lxumousa) to disable one.
* Template IDs differ between environments, so read them from the templates call rather than hardcoding them.
* If registration succeeded and your jobs complete but nothing ever arrives, your API user may not be fully set up for delivery. [Contact CrazyGoldFish](https://www.crazygoldfish.com/#comp-lxumousa).

### Events

| Event `name`                                            | Sent when                               | Fetch the result with                                                                                      |
| ------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `worksheet_initialize_metadata`                         | Worksheet subject matter is ready       | [Get worksheet metadata](/api-reference/endpoint/worksheet/v1-worksheet-meta-data-get)                     |
| `worksheet_question_configuration`                      | Worksheet question plan is ready        | [Get worksheet question configuration](/api-reference/endpoint/worksheet/v1-worksheet-question-config-get) |
| `worksheet_generation`                                  | Worksheet questions are ready           | [Get generated worksheet questions](/api-reference/endpoint/worksheet/v1-worksheet-generation-get)         |
| `worksheet_explanation_generation`                      | Worksheet explanations are ready        | [Get worksheet explanations](/api-reference/endpoint/worksheet/v1-worksheet-explanation-get)               |
| `lesson_plan_initialize_metadata`                       | Lesson plan metadata is ready           | `GET /lesson-plan/v1/metadata/{id}`                                                                        |
| `lesson_plan_finalize_metadata`                         | Lesson plan alignment is ready          | `GET /lesson-plan/v1/finalize-metadata/{id}`                                                               |
| `lesson_plan_instructional_framework_builder`           | Lesson plan content is ready            | `GET /lesson-plan/v1/instructional-framework-builder/{id}`                                                 |
| `lesson_plan_additional_enrichment_blocks`              | Enrichment blocks are ready             | `GET /lesson-plan/v1/additional-enrichment-block/{id}`                                                     |
| `student_action_plan`                                   | A student action plan is ready          | [Get student action plan](/api-reference/endpoint/action-plans/k12-v1-student-action-plan-get)             |
| `teacher_action_plan`                                   | A teacher action plan is ready          | [Get teacher action plan](/api-reference/endpoint/action-plans/k12-v1-teacher-action-plan-get)             |
| `class_track_analysis`                                  | A classroom recording has been analyzed | [Get analysis result](/api-reference/endpoint/class-track/v1-class-track-get)                              |
| `exam_evaluation_question_paper_extraction`             | Exam questions are extracted            | `GET /evaluation-platform/v1/exam/{exam_id}/questions/`                                                    |
| `exam_evaluation_model_answer_extraction`               | Model answers are extracted             | `GET /evaluation-platform/v1/exam/{exam_id}/model-answer/{model_answer_sheet_id}/`                         |
| `exam_evaluation_student_answer_sheet_evaluation`       | A student answer sheet is evaluated     | `GET /evaluation-platform/v1/exam/{exam_id}/answer-sheet/{ans_sheet_id}/pre-provisional-scores/`           |
| `assignment_evaluation_question_paper_extraction`       | Assignment questions are extracted      | `GET /assignment-evaluation/v1/assignment/{assignment_id}/questions/`                                      |
| `assignment_evaluation_model_answer_extraction`         | Assignment model answers are extracted  | `GET /assignment-evaluation/v1/assignment/{assignment_id}/model-answer/{model_answer_sheet_id}/`           |
| `assignment_evaluation_student_answer_sheet_evaluation` | A student assignment is evaluated       | `GET /assignment-evaluation/v1/assignment/{assignment_id}/answer-sheet/{ans_sheet_id}/score/`              |

Every event is sent whether the job succeeded or failed. Check `status`: `Completed` or `Failed`, and also `Partial` for action plans.

### Payloads

Payloads tell you which job finished. They don't include the result, so fetch it with the endpoint in the table above.

<CodeGroup>
  ```json Lesson Plan and Worksheet theme={null}
  {
    "id": "152764ca-1685-427c-aba7-4b92dcc0d60f",
    "name": "worksheet_generation",
    "display_name": "Worksheet Generation",
    "secret_hash": "a-long-random-string-you-generate",
    "status": "Completed"
  }
  ```

  ```json Exam Evaluation theme={null}
  {
    "status": "Completed",
    "exam_id": "d19e6868-98dd-4274-a2a5-4a616dd93ea7",
    "ans_sheet_id": "8a2f4c1e-5b3d-4e6f-9a7b-0c1d2e3f4a5b",
    "name": "exam_evaluation_student_answer_sheet_evaluation",
    "display_name": "Student Answer Sheet Evaluation",
    "secret_hash": "a-long-random-string-you-generate"
  }
  ```

  ```json Assignment Evaluation theme={null}
  {
    "status": "Completed",
    "assignment_id": "3c9e1f2a-7b4d-4a8e-9f1c-2d5e6a7b8c9d",
    "ans_sheet_id": "6e1d2c3b-4a5f-4e6d-8c7b-9a0f1e2d3c4b",
    "webhook_name": "assignment_evaluation_student_answer_sheet_evaluation",
    "secret_hash": "a-long-random-string-you-generate"
  }
  ```
</CodeGroup>

Exam payloads include `exam_id`, plus `model_answer_sheet_id` or `ans_sheet_id` for those events. Assignment payloads use `webhook_name` instead of `name`. Action plan and ClassTrack payloads use the same shape as the Lesson Plan and Worksheet example above: for `student_action_plan` the `id` is the answer sheet ID, for `teacher_action_plan` it is the exam ID, and for `class_track_analysis` it is the analysis ID. `secret_hash` is `null` if you registered the webhook without a secret.

<Note>
  A webhook can only be registered for an event that exists in your account's template list. If an event in this table is missing from [List event templates](/api-reference/endpoint/webhook/v1-get-webhook-template), [contact CrazyGoldFish](https://www.crazygoldfish.com/#comp-lxumousa) to have it added.
</Note>

### Handle deliveries

* **Verify the sender.** Compare `secret_hash` with the secret you registered, and reject the request if they don't match.
* **Respond within 5 seconds** with a `2xx` status. Do slow work after you respond.
* **Keep polling as a fallback.** Most events are sent once with no retry. `exam_evaluation_student_answer_sheet_evaluation` is retried once after 5 seconds if your endpoint doesn't return `200` or `201`.
* **Handle duplicates.** Because of retries, the same event can arrive twice.

<CodeGroup>
  ```python Python (Flask) theme={null}
  import hmac
  import os

  from flask import Flask, abort, request

  app = Flask(__name__)
  WEBHOOK_SECRET = os.environ["CRAZYGOLDFISH_WEBHOOK_SECRET"]


  @app.post("/webhooks/crazygoldfish")
  def crazygoldfish_webhook():
      event = request.get_json()
      if not hmac.compare_digest(event.get("secret_hash") or "", WEBHOOK_SECRET):
          abort(401)
      event_name = event.get("name") or event.get("webhook_name")
      queue_job(event_name, event)  # Fetch the result in a background worker
      return "", 200
  ```

  ```javascript Node.js (Express) theme={null}
  import crypto from "node:crypto";
  import express from "express";

  const app = express();
  const WEBHOOK_SECRET = process.env.CRAZYGOLDFISH_WEBHOOK_SECRET;

  function isValidSecret(received = "") {
    const a = Buffer.from(received);
    const b = Buffer.from(WEBHOOK_SECRET);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  app.post("/webhooks/crazygoldfish", express.json(), (req, res) => {
    if (!isValidSecret(req.body.secret_hash ?? "")) return res.sendStatus(401);
    const eventName = req.body.name ?? req.body.webhook_name;
    queueJob(eventName, req.body); // Fetch the result in a background worker
    res.sendStatus(200);
  });
  ```
</CodeGroup>
