Recursos
Content API SDKs

Content API SDKs

TypeScript and Python clients for the Content API: typed routes, retries, pagination, run polling, ETags, and webhook verification. Both are published, on npm and PyPI.

En esta página

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 generated 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

Both clients are published: the TypeScript client on npm and the Python client on PyPI. Both are at version 0.1.0, and their interfaces may still change between minor versions.

Install

bash
pnpm add @tutorflow/content-api-sdk
# or: npm install @tutorflow/content-api-sdk

The TypeScript package is ESM only, needs Node.js 20 or later, and ships its type declarations. The Python package needs Python 3.11 or later and is typed. Their source stays in TutorFlow's private repository; the packages are published under a proprietary license for use with TutorFlow.

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: { topic: '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. Both were last regenerated from the 2026-10-17 spec, which added the evaluation, concept, and mastery routes. Check the changelog for what changed.

¿Te resultó útil esta página?