Resources
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.

On this page

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. The request is refused by the HTTP layer before it reaches the Content API, so the body may not carry the error.code envelope.
Malformed JSON400, also refused before the Content API, so it may not carry error.code.

Field lengths

FieldLimit
Idempotency-Key on resource creates, actions, async generation, and renders255 characters. Longer returns 400.
Idempotency-Key on expansion jobsNo enforced limit.
API key name1 to 120 characters.
API key rateLimitPerMinute1 to 600. Default 60.
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.

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 scopesAny of the 4 scopes. An empty list returns 400.
Key classroomIdsNo enforced maximum. An empty list returns 400.

List pages

Resource lists return the { "data", "meta" } envelope described in List responses. The page size parameter is not the same on every list:

ListPage size parameterDefaultMaximumSort parametersSearch
Moduleslimit105,000field (default title), order (default ASC)search on title and description, case-insensitive
Courseslimit105,000field (default createdAt), order (default DESC)search on title and slug, case-sensitive
Videostake1050None. Always newest first by createdAt.None
Slideslimit105,000field (default title), order (default DESC)search on title and description, case-insensitive
Testslimit105,000field (default createdAt), order (default DESC)search on name and slug, case-insensitive
Gameslimit105,000field: title (default), createdAt, updatedAt. order (default DESC)search on title and description, case-insensitive
Simulationslimit105,000field: title (default), createdAt, updatedAt, subject. order (default DESC)search on title and description, case-insensitive, plus a subject filter
Webhook deliverieslimit20100Always newest first. Page with before.status, eventType filters

Every list takes page, starting at 1. order is ASC or DESC. On games and simulations a field outside the listed values returns 422. On modules, courses, slides, and tests, use createdAt, updatedAt, or the title column (title, or name on tests); other values are not supported. A page size above the maximum returns 422.

These routes return a plain array with no paging: GET /v1/content/classrooms, GET /v1/content/organizations, the API key list, the webhook list, and a course's GET .../chapters.

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.
Generated quiz items4 per lesson, at least 10 and at most 30.
Generated video scenesLesson count plus 1, at least 3 and at most 8.
Generated video target length30 seconds per lesson, at least 45 and at most 180 seconds.
Internal attempts per job3. A failed attempt is retried after 5 seconds, then 25 seconds, before the job reports failed.

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.

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. A complete wrapper that also handles 5xx, 402, and 409 is in Examples.

Time limits and retention

ItemValue
Idempotency replay window24 hours after the first request finished.
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.
Finished run on GET .../runReadable in last for 1 hour.
Signed videoUrl from render status and the video.render.completed webhook6 hours. Read the render status again for 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 log of key changes90 days.

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.

Was this page helpful?