Skip to main content
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:
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 },
);
$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,
    ],
);
For batch operations, every resource that supports generation also has a getStatus(ids[]) call so you can poll many at once:
const statuses = await hk.courses.getStatus([id1, id2, id3]);
// → [{ id: id1, status: "ready" }, { id: id2, status: "pending" }, ...]
$statuses = $hk->courses->getStatus([$id1, $id2, $id3]);
// → [{ id: $id1, status: 'ready' }, { id: $id2, status: 'pending' }, ...]

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:
{
  "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

MethodProduces
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:
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_...",
});
$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_...',
]);
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:
{
  "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.