资源
Content API Limits

Content API Limits

Every size, length, page, rate, and time limit the Content API enforces, with the parameter names that control them.

本页内容

Every number on this page is enforced by the API. Where a value has no enforced limit, the table says so; the request body cap still applies to it.

Request size

LimitValueWhat happens past it
JSON request body on /v1/content/**100 KB413 content_payload_too_large.
Malformed JSON400 content_invalid_request.
File uploaded to an upload URLPer purpose, 20 MB to 500 MBThe file does not pass through the Content API, so the 100 KB limit does not apply. See Asset Uploads.

Field lengths

FieldLimit
Idempotency-Key on resource creates, actions, async generation, renders, and expansion jobs255 characters. Longer returns 400.
X-Request-Id or Request-Id you send1 to 128 characters of letters, digits, ., _, and -. Anything else is replaced with a generated UUID.
List search200 characters. Longer returns 422.
API key name1 to 120 characters.
API key rateLimitPerMinute1 to 600. Default 60.
API key monthlyCreditLimitA whole number from 1 to 10,000,000, or null for no limit.
API key rotation gracePeriodHours0 to 168.
Webhook secret rotation gracePeriodHours0 to 168.
Webhook url2,048 characters, https, resolving to a public address.
Webhook secret16 to 256 characters.
Course chapter title1 to 255 characters.
Course lesson title1 to 255 characters.
Course lesson description2,000 characters.
Course lesson lecture, content100,000 characters. lecture cannot be empty.
Game or simulation revision feedback1 to 2,000 characters. The history note keeps the first 200.
Titles, descriptions, and bodies of modules, courses, videos, scenes, slides, tests, games, and simulationsNo enforced limit beyond the 100 KB body.
Game and simulation metadata, brief, referenceMarkdownNo enforced limit beyond the 100 KB body.
Webhook delivery log eventType filter64 characters.
Test generation topic, instructionstopic 1 to 4,000 characters; instructions up to 4,000.
Module generation topic1 to 4,000 characters.
Slide deck generation topic, instructionstopic 1 to 4,000 characters; instructions up to 4,000.
Module generation referenceText100,000 characters.
Course generation prompt, title, descriptionprompt 1 to 4,000 characters; title up to 255; description up to 4,000.
Generation language35 characters.
Upload filename255 characters.
Learner list q100 characters.
Learner invitation name100 characters.
Learner invitation locale35 characters, a BCP 47 tag.
cursor on cursor-paged lists200 characters.

Ids per request

RequestLimit
PATCH .../courses/{courseId}/chapters/reorder chapterIds1 to 500, unique, and exactly the course's chapter ids.
PATCH .../chapters/{chapterId}/lessons/reorder lessonIds1 to 500, unique, and exactly the chapter's lesson ids.
PATCH .../videos/{videoId}/scenes/reorder sceneIdsAt least 1. No enforced maximum.
Key classroomIdsNo enforced maximum. An empty list returns 400.
Key scopesAny of the 6 scopes. An empty list returns 400.
Test generation itemCount1 to 30. itemTypes must have exactly itemCount entries.
Course generation lessonCount1 to 30. Default 12.
Slide deck generation slideCount5 to 20. Default 10.
Learner invitation courseIds, testIdsUp to 100 each, unique, all in the classroom.
Course enrollment learnerIds1 to 100, unique, all in the classroom.

List pages

Resource lists (modules, courses, videos, slides, tests, games, and simulations) return the { "data", "meta" } envelope described in List responses, and all take the same query parameters:

ParameterDefaultBehavior
page1Starts at 1. Below 1 or not an integer returns 422.
limit20Items per page. A value above 100 is clamped to 100, not refused; meta.take reports the size used. Below 1 or not an integer returns 422.
takeDeprecated alias of limit, with the same default and clamp. limit wins when both are sent. See Current deprecations.
fieldcreatedAtThe sort column, from the list's allowed values below. Anything else returns 422 with error.allowedValues.
orderDESCASC or DESC. Ties are broken on id in the same direction, so a page never skips or repeats a row.
searchCase-insensitive substring of the title (name on tests). % and _ match themselves. Up to 200 characters.
updatedSinceAn ISO 8601 date-time, such as 2026-09-30T00:00:00Z or one with an offset. Only rows with updatedAt at or after it are listed. Not ISO 8601 returns 422.
Listfield valuesOther filters
CoursescreatedAt, updatedAt, title, slugisIncludeStats
TestscreatedAt, updatedAt, name, slug, and title (same as name)isIncludeStats
Modules, slides, videoscreatedAt, updatedAt, title
Gamestitle, createdAt, updatedAt
Simulationstitle, createdAt, updatedAt, subjectsubject

Deleted rows are not listed. To learn about deletions, use the resource.deleted webhook.

The webhook delivery log pages differently: limit 1 to 100 (default 20), always newest first, with before for the next page and status and eventType filters. See Delivery log.

The credit history, audit log, generation runs, learners, enrollments, and test results page with a cursor instead: limit (default 20, values above 100 clamped to 100) and cursor, newest first, with { "data", "meta": { "limit", "hasNextPage", "nextCursor" } }. See Paging.

These routes return a plain array with no paging: GET /v1/content/classrooms, GET /v1/content/organizations, the API key list, and the webhook list. A course's GET .../chapters returns the whole curriculum with no paging, as { "data", "unassignedLessons" }.

Source JSON expansion

LimitValue
Levels per job1. The first level that has at least one lesson is expanded; other levels in the payload are ignored. See Source JSON Format.
Lessons per levelAt least 1. No enforced maximum beyond the 100 KB body.
Lesson fields read per lessonid, title or name, type, plus the first 6 other fields. Later fields are stored with the job but not used for generation.
requestedOutputsUp to 3 distinct values. Duplicates collapse, and outputs always run in the order interactive_module, summary_video, expanded_quiz.
Video and quiz outputsCreated empty: the summary_video video has no scenes and the expanded_quiz test has no items. Add them with the resource routes.
Internal attempts per job3. A failed attempt is retried after 5 seconds, then 25 seconds, before the job reports failed.

Upload limits

LimitValue
Upload URLs per organization1,000 in any rolling 24 hours, across all keys.
Declared upload bytes per organization20 GB (21,474,836,480 bytes) in any rolling 24 hours.
Upload URLs and bytes with test keys50 uploads and 500 MB (524,288,000 bytes) in any rolling 24 hours, per organization, separate from the live allowance.
Upload URL lifetime15 minutes.
Size per file20 MB to 500 MB by purpose. See Purposes and limits.

Past a daily limit, asking for an upload URL answers 429 content_asset_daily_limit_reached with Retry-After.

Webhook delivery

ItemValue
Delivery after an eventUsually within about a second.
Order per endpointOne delivery at a time, oldest first.
Pause after a failed attemptAbout 30 seconds for that endpoint.
Automatic turn-offAt least 20 failed attempts in a row and at least 3 days failing, both together.
replayFailedSinceAt most 7 days ago; at most 10,000 events per request.

See Endpoint health.

Rate limits

Each key has its own budget, rateLimitPerMinute (1 to 600, default 60), counted over a 60-second window. The budget is shared across every route the key calls, and a busy key never uses up another key's budget. Requests refused with 401, or with 403 for a missing scope or classroom, are not counted against the key.

Every counted response carries:

HeaderMeaning
X-RateLimit-LimitThe key's rateLimitPerMinute.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetSeconds until the current window resets.
Retry-AfterOnly on 429. Seconds to wait before the next request.

A request over the limit returns 429:

JSON
{
  "error": {
    "code": "content_rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 12 seconds",
    "retryAfterSeconds": 12,
    "status": 429
  }
}

Wait for Retry-After, then send the same request again with the same Idempotency-Key:

JavaScript
// Node.js 18+
async function fetchWithRateLimit(url, init, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, init)
 
    if (response.status !== 429 || attempt === maxAttempts) {
      return response
    }
 
    const waitSeconds = Number(response.headers.get('retry-after') ?? '1')
    await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000))
  }
}

To stay under the limit in a batch, read X-RateLimit-Remaining and pause until X-RateLimit-Reset when it reaches 0. Browser clients can read these headers, and Retry-After: the Content API lists them in Access-Control-Expose-Headers. A complete wrapper that also handles 5xx, 402, and 409 is in Examples.

Failed authentication limit

Separate from the per-key budget, each client address may fail authentication (401) up to 300 times in a one-minute window. Once it reaches that, every request from the address to a tf_content_ route answers 429 content_rate_limit_exceeded, with Retry-After and error.retryAfterSeconds, until the window ends. The check runs before the key is looked up, so it applies even to requests with a valid key from that address.

Only 401s count, so valid traffic from one address is never limited by this; the per-key limits above apply to it instead. These 429s carry no X-RateLimit-* headers. If you see one, stop retrying with the bad key: fix the key, wait Retry-After, then resume.

Time limits and retention

ItemValue
Idempotency replay window24 hours after the first request. Applies to expansion jobs too, except jobs created before 2026-09-30, whose keys have no expiry.
Idempotency takeoverA request with the same key still marked in progress after 5 minutes can be taken over by a retry.
Game or simulation build or revision30 minutes at most. A run that reaches it fails and charges nothing.
Test generation run20 minutes.
Module generation run30 minutes.
Course generation run15 minutes plus 8 minutes per lesson.
Slide deck generation run10 minutes plus 8 minutes per batch of four pages.
Attempts per generation step3, counting attempts cut short by a server restart.
Expansion job attempts3, 5 and then 25 seconds apart. content.failed is sent after the last.
Finished run on GET .../runReadable in last for 1 hour.
Signed videoUrl from render status6 hours. Read the render status again for a new link.
Signed videoUrl in the video.render.completed webhook6 hours from each delivery attempt. Every attempt and redelivery signs a new link.
Webhook receiver response10 seconds per attempt.
Webhook attempts9 over about 23 hours.
Webhook redirects followed3 hops, 307 and 308 only.
Webhook delivery log30 days.
Audit log90 days.
Upload records90 days. Uploaded files are not deleted with them.
Pending upload settled as unconfirmed or abandonedAfter 1 hour without complete.
Learner invitationExpires after 24 hours.
Key monthly credit limitResets at 00:00 UTC on the 1st of each month.
API key expiry warningSent once, 7 days or less before expiresAt. See Expiry warnings.
Failed authentication window60 seconds, 300 401s per address.

No enforced limit

These have no limit in the API today. If one is added, it is announced in the Changelog first, as described in Versioning and Deprecation.

  • Webhook endpoints per organization.
  • Webhook payload size. Payloads are small JSON objects; they do not carry built HTML or media.
  • Content API keys per organization.
  • Classrooms per key allowlist.
  • Resources per classroom.
  • Concurrent expansion jobs per key, beyond the rate limit.

这个页面对你有帮助吗?