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

# AI generation

> How async generation works, polling vs webhooks, and grounded research.

Anywhere you can author content by hand, you can also generate it. The
generation API is **async-first** because most of it takes 10s–60s.

## The lifecycle

```
       ┌──────────────────────────────────────┐
       │ 1. Call *.generate*()                │
       │    → returns { id, status: "pending" }│
       └──────────────────────────────────────┘
                         │
                         ▼
       ┌──────────────────────────────────────┐
       │ 2. Backend runs the generation       │
       │    (research → outline → write → QA) │
       └──────────────────────────────────────┘
                         │
                         ▼
       ┌──────────────────────────────────────┐
       │ 3. Resource transitions to "ready"   │
       │    or "error"                        │
       └──────────────────────────────────────┘
```

You learn about completion by **polling** or by **webhooks**.

## Polling

The SDK ships a tiny `poll()` helper:

<CodeGroup>
  ```ts TypeScript theme={null}
  const course = await hk.courses.create({ name: "Intro to SR", language: "en" });

  await hk.courses.generateSyllabus(course.id, {
    brief: "An introductory course on special relativity...",
  });

  const ready = await hk.poll(
    () => hk.courses.retrieve(course.id),
    { until: c => c.status === "ready", interval_ms: 2000, timeout_ms: 120_000 },
  );
  ```

  ```php PHP theme={null}
  $course = $hk->courses->create(['name' => 'Intro to SR', 'language' => 'en']);

  $hk->courses->generateSyllabus($course->id, [
      'brief' => 'An introductory course on special relativity...',
  ]);

  $ready = $hk->poll(
      fn () => $hk->courses->retrieve($course->id),
      [
          'until' => fn ($c) => $c->status === 'ready',
          'interval_ms' => 2000,
          'timeout_ms' => 120_000,
      ],
  );
  ```
</CodeGroup>

For batch operations, every resource that supports generation also has a
`getStatus(ids[])` call so you can poll many at once:

<CodeGroup>
  ```ts TypeScript theme={null}
  const statuses = await hk.courses.getStatus([id1, id2, id3]);
  // → [{ id: id1, status: "ready" }, { id: id2, status: "pending" }, ...]
  ```

  ```php PHP theme={null}
  $statuses = $hk->courses->getStatus([$id1, $id2, $id3]);
  // → [{ id: $id1, status: 'ready' }, { id: $id2, status: 'pending' }, ...]
  ```
</CodeGroup>

## Webhooks

Recommended in production. Subscribe to events in the dashboard:

* `course.syllabus_generated`
* `lesson.content_generated`
* `activity.generated`
* `submission.graded`

Hawkings POSTs to your URL with:

```json theme={null}
{
  "id": "evt_01HX9...",
  "type": "course.syllabus_generated",
  "data": { "course_id": "crs_01HX9...", "status": "ready" },
  "created_at": "2026-05-10T12:34:56Z"
}
```

Verify the signature with `hk.webhooks.verify(body, signature, secret)`.

## What you can generate

| Method                               | Produces                                           |
| ------------------------------------ | -------------------------------------------------- |
| `courses.generateSyllabus(id, ...)`  | Units, lessons, learning objectives.               |
| `courses.adaptSyllabus(id, ...)`     | Re-tailors an existing syllabus to a new audience. |
| `courses.generateImage(id, ...)`     | Cover image for the course.                        |
| `lessonContents.generate(...)`       | Long-form HTML reading for a lesson.               |
| `lessonContents.generateImages(...)` | Inline diagrams and illustrations.                 |
| `activities.generate(...)`           | A mix of activities for a lesson.                  |
| `activities.create({ type: ..., })`  | A specific activity (also async).                  |
| `tags.generate({ source })`          | Tags extracted from a body of text.                |
| `submissions.gradeWithAi(id)`        | Score + rationale for a submission.                |

## Grounded research (advanced)

By default, generation uses the model's training. You can ground it in
specific sources for higher factual accuracy:

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.research.create({
    query: "Recent (2024-2026) experimental tests of special relativity",
    sources: [
      { type: "url", url: "https://arxiv.org/abs/2403.12345" },
      { type: "pdf", file_id: "fil_..." },
    ],
  });

  // Then attach the research to a generation
  await hk.lessonContents.generate({
    lesson_id: "lsn_123",
    research_id: "res_...",
  });
  ```

  ```php PHP theme={null}
  $hk->research->create([
      'query' => 'Recent (2024-2026) experimental tests of special relativity',
      'sources' => [
          ['type' => 'url', 'url' => 'https://arxiv.org/abs/2403.12345'],
          ['type' => 'pdf', 'file_id' => 'fil_...'],
      ],
  ]);

  // Then attach the research to a generation
  $hk->lessonContents->generate([
      'lesson_id' => 'lsn_123',
      'research_id' => 'res_...',
  ]);
  ```
</CodeGroup>

Research artefacts are reusable across generations. They expire after
30 days unless pinned.

## Cost & quotas

Every generation method consumes credits (see your dashboard for the
per-call cost). Calls can be capped via per-key quotas; exceeding a
quota throws `RateLimitError` with `code: "quota_exceeded"`.

## When generation fails

`status: "error"` resources carry an `error` object:

```json theme={null}
{
  "status": "error",
  "error": {
    "type": "generation_failed",
    "code": "model_refusal",
    "message": "The model refused to generate content for this prompt.",
    "retriable": true
  }
}
```

If `retriable: true`, calling the same `*.generate*()` again with the
same parameters is safe. If it's `false`, fix the input and try again.
