Skip to main content

Rate Limits

The Prezent API enforces three independent kinds of limits. They are evaluated independently, and for any given request the most restrictive one binds first. All three are configurable per company — contact your CSM to adjust them.

This page is self-contained: every number you need is in the tables below. For the broader error catalog and async patterns, see the Developer Guide.

The three tiers at a glance

TierEnforced atScopeWindow429 error.code
1. Gateway throttleAWS API Gateway edgePer API key, all endpointsPer-second + per-dayTOO_MANY_REQUESTS
2. Per-minute rate limitApplication (Lambda)Per company × API category60-second slidingRATE_LIMIT_EXCEEDED
3. Annual usage quotaApplication (Lambda)Per API key, per billable operationRolling yearUSAGE_LIMIT_EXCEEDED

A fourth code, MAX_RETRIES_EXCEEDED, is returned when an internal retry budget for a downstream operation is exhausted (also 429).

Tier 1 — Gateway throttle

A per-API-key request-rate and daily-request cap enforced at the AWS API Gateway edge. It applies to every endpoint, regardless of category.

SettingDefault
Steady request rate10 requests / second per API key
Burst capacity5 requests
Daily request quota1,000 requests / day per API key, across all endpoints

Custom usage plans can raise these caps per customer — the values above are the standard defaults provisioned for a new key. When a key exceeds the throttle, API Gateway returns HTTP 429 with the canonical error envelope and error.code: "TOO_MANY_REQUESTS".

The gateway throttle is enforced before your request reaches the application, so a gateway 429 does not carry the X-RateLimit-* headers described below — those are computed by the application for the per-minute limit (Tier 2). Treat a gateway 429 as a signal to back off across all of your traffic on that key.

Tier 2 — Per-minute rate limits

Application-level limits, enforced per company × API category over a 60-second sliding window. Three categories exist in production today:

CategoryEndpoints in this category
auto_generatorAll /api/v1/autogenerator/* (and the /api/v1/autogenerations/* REST aliases), plus /api/v1/audiences, /api/v1/themes, /api/v1/upload, /api/v1/validate, and /api/v1/file-access
template_converterAll /api/v1/template-converter/* and /api/v2/template-converter/* (and the /api/v1/template-conversions/* REST aliases)
scimAll /api/v1/scim/*
SettingDefault
Requests per windowConfigured per company — contact your CSM
Window length60 seconds, sliding

The per-window request count is configured per company × category; there is no platform-wide default value. The limiter only engages once a limit has been configured for your company and category — until then, only Tier 1 (the gateway throttle) and Tier 3 (annual quotas) apply. In practice the gateway throttle's 1,000 requests / day default is the most restrictive limit for most workloads and will bind first.

Endpoints not classified under one of the three categories above (for example, health checks) are not throttled by the per-minute limiter — only the gateway throttle applies to them.

When this limit is exceeded the application returns HTTP 429 with error.code: "RATE_LIMIT_EXCEEDED".

Tier 3 — Annual usage quotas

Per-API-key caps on specific billable operations, tracked across a rolling year. Default quotas for new customers (configurable per company):

OperationDefault quota (per year)Counted by
Slide generation — POST /api/v1/autogenerator, POST /api/v1/autogenerator/regenerate50,000 / yearsuccessful slides generated
Presentation download — POST /api/v1/autogenerator/download1,000,000 / yearsuccessful downloads
Template-Converter start — POST /api/v1/template-converter/startNegotiated per customersuccessful TC starts
Template-Converter download — GET /api/v1/template-converter/download/{callback_id}Negotiated per customersuccessful TC downloads

Quotas are tracked on the API key. All other endpoints (audiences, themes, upload, file-access, SCIM, status polls, etc.) do not consume any annual quota. When a key exceeds its quota the API returns HTTP 429 with error.code: "USAGE_LIMIT_EXCEEDED".

The four 429 error codes

Every limit surfaces as HTTP 429 with the standard error envelope. The error.code tells you which tier you hit and how to react:

error.codeTierTriggered whenWhat to do
TOO_MANY_REQUESTS1 — Gateway throttleYou exceed 10 req/s, the 5-request burst, or 1,000 req/day on the keyBack off all traffic on the key; retry after a delay
RATE_LIMIT_EXCEEDED2 — Per-minute rate limitYour company exceeds its configured per-category limit in the 60-second windowSlow this category; retry after the window resets
USAGE_LIMIT_EXCEEDED3 — Annual usage quotaThe key reaches its yearly cap for a billable operationContact your CSM to raise the quota
MAX_RETRIES_EXCEEDEDInternal retry budgetAn internal retry budget for a downstream operation is exhaustedRetry the request after a delay; if it persists, contact support

error.code values are stable strings drawn from the error catalog — they never change meaning or HTTP status once published.

Error envelope

{
"success": false,
"data": null,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Company rate limit exceeded. Please try again later.",
"details": {
"category": "auto_generator"
}
}
}

Response headers

The headers below describe the per-minute, per-category limit (Tier 2) — they are the values the application computes for your company and category.

HeaderDescription
X-RateLimit-LimitConfigured requests-per-window for the category.
X-RateLimit-RemainingApproximate requests remaining in the current 60-second window.
X-RateLimit-ResetUnix timestamp (seconds) at which the window resets.
Retry-AfterSeconds the client should wait before retrying.
note

X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset are returned on successful (2xx) responses from rate-limited endpoints, and alongside Retry-After on a per-category RATE_LIMIT_EXCEEDED 429 — so you can pace yourself before hitting the wall and know how long to wait when you do. They always describe the Tier-2 per-category limit.

The gateway throttle (Tier 1) is enforced at the API Gateway edge, and the annual quota (Tier 3, USAGE_LIMIT_EXCEEDED) is not a per-minute window — neither response carries X-RateLimit-* / Retry-After. For those, use error.code in the body as the signal and apply the jittered backoff below.

Handling 429: Retry-After + jittered exponential backoff

The robust pattern is: honour Retry-After when it is present, and otherwise fall back to jittered exponential backoff (so a fleet of clients doesn't retry in lockstep and re-trigger the limit).

Bash / curl

#!/usr/bin/env bash
# Retry a Prezent API call on 429 with Retry-After + jittered backoff.
set -euo pipefail

URL="https://api.prezent.ai/api/v1/autogenerator"
MAX_ATTEMPTS=6
BASE=1 # base backoff in seconds
CAP=60 # max backoff in seconds

attempt=0
while :; do
attempt=$((attempt + 1))

# -s body to stdout, write the HTTP code + Retry-After to a side channel
resp="$(curl -s -w '\n%{http_code} %{header_json}' \
-X POST "$URL" \
-H "Authorization: Bearer $PREZENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"A 5-slide deck on renewable energy","template_id":"tmpl-1"}')"

code="$(printf '%s' "$resp" | tail -n1 | awk '{print $1}')"

if [ "$code" != "429" ]; then
printf '%s\n' "$resp" | sed '$d' # print the body, drop the status line
exit 0
fi

if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then
echo "giving up after $attempt attempts" >&2
exit 1
fi

# Prefer the server's Retry-After if it sent one.
retry_after="$(printf '%s' "$resp" | tail -n1 \
| grep -oiE '"retry-after":[^,}]*[0-9]+' | grep -oE '[0-9]+' | head -n1 || true)"

if [ -n "${retry_after:-}" ]; then
sleep_for="$retry_after"
else
# Exponential backoff capped at $CAP, plus full jitter in [0, backoff].
backoff=$(( BASE * (2 ** (attempt - 1)) ))
[ "$backoff" -gt "$CAP" ] && backoff="$CAP"
sleep_for=$(( RANDOM % (backoff + 1) ))
fi

echo "429 received; sleeping ${sleep_for}s (attempt ${attempt})" >&2
sleep "$sleep_for"
done

Python

import random
import time
import requests

URL = "https://api.prezent.ai/api/v1/autogenerator"
MAX_ATTEMPTS = 6
BASE = 1.0 # base backoff in seconds
CAP = 60.0 # max backoff in seconds


def call_with_retry(api_key: str, payload: dict) -> dict:
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}

for attempt in range(1, MAX_ATTEMPTS + 1):
resp = requests.post(URL, headers=headers, json=payload)

if resp.status_code != 429:
resp.raise_for_status()
return resp.json()

if attempt == MAX_ATTEMPTS:
resp.raise_for_status() # surface the final 429

# Honour Retry-After if the server sent one...
retry_after = resp.headers.get("Retry-After")
if retry_after is not None:
delay = float(retry_after)
else:
# ...otherwise full-jitter exponential backoff.
backoff = min(CAP, BASE * (2 ** (attempt - 1)))
delay = random.uniform(0, backoff)

code = resp.json().get("error", {}).get("code", "RATE_LIMIT_EXCEEDED")
print(f"429 ({code}); sleeping {delay:.1f}s (attempt {attempt})")
time.sleep(delay)

raise RuntimeError("unreachable")

Best practices

  • Honour Retry-After first. When the header is present, wait at least that long. Only fall back to computed backoff when it is absent.
  • Use jitter. Full jitter (a random delay in [0, backoff]) prevents a thundering herd of clients from retrying in lockstep.
  • Cap your backoff and your attempts. Unbounded retries turn a transient 429 into a stuck client.
  • Branch on error.code, not just the status. A USAGE_LIMIT_EXCEEDED (annual quota) will not clear by retrying — it needs a quota increase, not backoff.
  • Spread bulk work out. The gateway's 1,000 requests / day default is usually the first wall you hit; batch and pace large jobs.

See also