Resources
Content API SDKs

Content API SDKs

TypeScript and Python clients for the Content API: typed routes, retries, pagination, run polling, ETags, and webhook verification. Install from source until they are published.

On this page

Two clients wrap the Content API so an integration does not have to write the plumbing itself. Both are generated from the same OpenAPI document the API reference publishes, so every route, request body, and response is typed, and both add the same helpers:

  • Retries on 429 after Retry-After, and on 5xx or dropped connections when repeating the request is safe.
  • Idempotency keys that stay the same across retries, so a retried POST is answered with the first result.
  • Typed errors with code, status, requestId, retryAfterSeconds, and currentETag from the error envelope.
  • Pagination over both list shapes: page-numbered lists and cursor lists.
  • Waiting for runs: generation runs, game and simulation builds, and video renders.
  • Webhook verification of X-Content-Integration-Signature-V2, including the secret rotation grace period, with livemode on every event.
LanguagePackageBuilt onSource
TypeScript and JavaScript (Node.js 20+)@tutorflow/content-api-sdkopenapi-fetchpackages/content-api-sdk
Python 3.11+tutorflow-contenthttpxsdks/python

The SDKs are not on npm or PyPI yet. Install them from source as shown below. Until they are published, their interfaces may still change between versions.

Install from source

bash
git clone https://github.com/geek-haus/tutorflow-monorepo.git
cd tutorflow-monorepo
pnpm install
pnpm --filter @tutorflow/content-api-sdk build
 
# In your project:
pnpm add /path/to/tutorflow-monorepo/packages/content-api-sdk

Make a call

Keep the key on your server. Both clients send it as a bearer token and refuse a value that does not start with tf_content_. A test key (tf_content_test_...) works the same way against the sandbox classroom.

TypeScript
import { createClient, unwrap } from '@tutorflow/content-api-sdk'
 
const client = createClient({ apiKey: process.env.TUTORFLOW_CONTENT_API_KEY! })
 
// unwrap() returns the data, or throws a ContentApiError.
const classrooms = unwrap(await client.GET('/v1/content/classrooms'))

Generate a course and wait for it

TypeScript
import { waitForRun, withIdempotencyKey } from '@tutorflow/content-api-sdk'
 
const accepted = unwrap(
  await client.POST(
    '/v1/content/classrooms/{classroomId}/courses/generate',
    withIdempotencyKey({
      params: { path: { classroomId } },
      body: { prompt: 'Fractions for Grade 5', lessonCount: 6, language: 'en' },
    }),
  ),
)
 
const run = await waitForRun(client, { type: 'generation', classroomId, runId: accepted.runId })
if (run.status === 'failed') throw new Error(run.error ?? 'Generation failed')

Waiting does not throw when the run failed; check the status it returns. The same call waits for a game or simulation build (type: 'build', or wait_for_build in Python) and a video render (type: 'render', or wait_for_render). A webhook tells you the same without polling, so prefer one wherever you can receive it.

List every item

paginate follows meta.page on page-numbered lists and meta.nextCursor on cursor lists. Keep the other filters the same on every call.

TypeScript
import { paginate } from '@tutorflow/content-api-sdk'
 
for await (const course of paginate((next) =>
  client.GET('/v1/content/classrooms/{classroomId}/courses', {
    params: { path: { classroomId }, query: { ...next, limit: 100 } },
  }),
)) {
  console.log(course.id, course.title)
}

Read and write with ETags

The TypeScript SDK has getIfChanged and updateIfMatch. getIfChanged sends If-None-Match together with Cache-Control: max-age=0: fetch() adds Cache-Control: no-cache to conditional requests on its own, and the API never answers 304 to that, so without the explicit header every read downloads the full body again. updateIfMatch sends If-Match and throws ContentPreconditionFailedError with currentETag when someone changed the resource since your read.

TypeScript
import { getIfChanged, updateIfMatch } from '@tutorflow/content-api-sdk'
 
const read = await getIfChanged(savedETag, ({ headers }) =>
  client.GET('/v1/content/classrooms/{classroomId}/courses/{id}', { params: { path: { classroomId, id } }, headers }),
)
 
if (!read.notModified) {
  await updateIfMatch(read.etag!, ({ headers }) =>
    client.PATCH('/v1/content/classrooms/{classroomId}/courses/{id}', {
      params: { path: { classroomId, id } },
      body: { title: 'Fractions for Grade 5 (2026)' },
      headers,
    }),
  )
}

Verify webhooks

Pass the raw body, before any JSON parsing, the X-Content-Integration-Signature-V2 header, and your secret. During a rotation, pass both secrets. Verification accepts a timestamp up to 300 seconds from your clock and returns the event with livemode.

TypeScript
import { isWebhookEventType, verifyWebhook } from '@tutorflow/content-api-sdk'
 
const event = await verifyWebhook({
  payload: rawBody,
  header: req.get('X-Content-Integration-Signature-V2'),
  secret: [process.env.TUTORFLOW_WEBHOOK_SECRET!, process.env.TUTORFLOW_WEBHOOK_SECRET_PREVIOUS ?? ''],
})
 
if (isWebhookEventType(event, 'course.generation.completed') && event.livemode) {
  await queueCourseImport(event.event.id, event.resourceId)
}

A failed check raises WebhookVerificationError with a reason: missing_header, malformed_header, timestamp_out_of_tolerance, no_matching_signature, or invalid_payload. Answer it with 400. The algorithm is the one described in Verify the signature.

Retry rules

AnswerRetriedWait
429Always: the request was refused before any workRetry-After, up to 60 seconds. A longer wait is returned to you as the 429.
500, 502, 503, 504, connection failureOnly for GET, PUT, DELETE, and requests with an Idempotency-KeyBackoff from 0.5 seconds, doubling with jitter, at most 8 seconds
Anything elseNo

Two retries by default. Change it with createClient({ retry: { maxRetries: 4 } }) or create_client(key, retry=RetryConfig(max_retries=4)), or turn retries off with retry: false or retry=None.

Keeping the SDKs current

Both SDKs are generated from apps/frontend-admin/public/resources/integrations/openapi.json. When the API changes, the file is regenerated from the backend and the SDK code with it; pnpm --filter @tutorflow/content-api-sdk check:generated and python scripts/generate.py --check (in sdks/python) fail when the generated code is stale. Check the changelog for what changed.

Was this page helpful?