Rate Limits & Quotas

ContentHub enforces two types of limits: generation quotas (based on your plan) and request rate limits (per API key). Understanding these prevents unexpected failures in automated integrations.

Generation quotas

Each workspace has a monthly limit on the number of AI-generated content pieces. This applies equally whether generation is triggered via the dashboard or the REST API (POST /api/v1/content/:id/generate).

PlanContent pieces / monthProjects
Free Trial51
VIP Trial303
Plus150Unlimited
Pro500Unlimited
EnterpriseCustomCustom

Quotas reset at the start of each calendar month. The current usage is visible in your workspace dashboard sidebar. When the limit is reached, generation attempts return:

json
HTTP/1.1 429 Too Many Requests

{
  "error": {
    "code": "QUOTA_EXCEEDED",
    "message": "Monthly content limit reached (30 pieces). Please upgrade your plan."
  }
}

API request rate limits

ContentHub enforces per-minute token-bucket rate limits on every REST endpoint, checked both per API key and per workspace (so multiple keys on one workspace can't collectively exceed the workspace-wide cap). Limits are tiered by endpoint cost:

TierExamplesPer key / minPer workspace / min
readAll GET endpoints300900
writeContent/settings/project mutations, archive/unarchive60180
expensivegenerate, publish, create-post, regenerate1020
egressConnection test, integration sync2040

Exceeding a limit returns 429 with error.code: "RATE_LIMITED" and a Retry-After header (seconds until the current 1-minute window resets). This is distinct from QUOTA_EXCEEDED above — rate limiting means "you're calling too fast," quota means "you're out of monthly generation budget."

json
HTTP/1.1 429 Too Many Requests
Retry-After: 42

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Retry after 42 second(s)."
  }
}

Handling 429 responses

When you receive a 429 response, follow this strategy:

  1. Read the error.code field to distinguish QUOTA_EXCEEDED (monthly limit) from RATE_LIMITED (calling too fast)
  2. For QUOTA_EXCEEDED: pause generation calls until the next month reset — non-generate endpoints are unaffected
  3. For RATE_LIMITED: wait the duration in Retry-After seconds before retrying
  4. Use exponential backoff with jitter for retries to avoid thundering-herd issues
typescript
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const res = await fetch(url, options)
    if (res.status !== 429) return res

    const retryAfter = res.headers.get("Retry-After")
    const delay = retryAfter
      ? parseInt(retryAfter, 10) * 1000
      : Math.pow(2, attempt) * 1000 + Math.random() * 500

    if (attempt < maxRetries) await new Promise(r => setTimeout(r, delay))
  }
  throw new Error("Max retries exceeded")
}

Pagination and large datasets

All list endpoints return paginated results. The default page size is 20; the maximum is 100. For bulk operations over large catalogs, iterate through pages sequentially rather than issuing many parallel requests:

typescript
async function fetchAllProducts(apiKey: string) {
  const products = []
  let page = 1
  let pages = 1

  while (page <= pages) {
    const res = await fetch(`/api/v1/products?page=${page}&limit=100`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    })
    const { data, meta } = await res.json()
    products.push(...data)
    pages = meta.pages
    page++
  }

  return products
}

Idempotency for write operations

POST and PATCH operations on the ContentHub REST API are not currently idempotent. If you retry a failed POST /api/v1/products request, you may create duplicate products. To avoid this:

  • Check for existing records with GET /api/v1/products before creating
  • Prefer PATCH over POST when updating existing records
  • Only retry on network errors (timeouts, connection resets) — not on 4xx responses

Next steps