Rate Limiting
The Typograph API enforces rate limits to ensure fair usage and maintain service quality for all users. We use a multi-layer rate limiting system with tiered subscriptions and endpoint categories.
Organization-Based Rate Limiting
Rate limits in the Typograph API are tied to organizations, not individual clients or users. When you make an API request, the rate limits applied are determined by your organization's subscription plan.
How It Works
- Every OAuth client belongs to an organization - When you create an OAuth client, it's associated with your organization
- Organization subscription determines limits - Your organization's plan (Free, Pro, Enterprise) determines the rate limits
- All clients in an organization share quota - Rate limits are pooled at the organization level
- On the Free plan, converter and publisher work also counts per person - see below
- The portal is not the API - the portal's own requests count toward none of these categories, with two exceptions: publisher and converter work started there counts, for the organization that owns the project or template (see Plans & Billing), and the requests of its AI assistant count in the
aicategory
One Free Organization per Person
Every organization on the Free plan has limits of its own. So that they do not add up:
- You own one organization on the Free plan. Creating another while you own a free one is refused with
409andfree_organization_limit_reached(POST /v1/identity/organizations). A second organization goes on a paid plan. - Converter and publisher requests on the Free plan count per person as well, across every free organization you work in, against the Free limits below. A token that acts for a person (one from signing in, or an MCP client) is counted for its organization and for that person; the first limit reached refuses the request, with the error codes below and a message that says it counts per person. Paid plans and the other categories count per organization only, and so does an app's client-credentials token, which acts for no person.
Rate Limit Tiers
Rate limits vary based on your subscription plan. Every plan carries limits for five endpoint categories — general, file, converter, publisher, and webhook — across four time windows. A sixth category, ai, counts the requests of the portal's AI assistant (see Endpoint Categories).
| Plan | Use Case |
|---|---|
| Anonymous | Unauthenticated requests, restricted to general category |
| Free | Development and low-volume integrations |
| Pro | Standard production workloads |
| Enterprise | High-volume production |
Unauthenticated requests are only accepted on general category endpoints (OAuth discovery, token issuance). Every other category requires authentication.
Anonymous Tier
Baked into the gateway — always the same values.
| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |
|---|---|---|---|---|---|
| general | 100 | 1,000 | 10,000 | 30 | 20 |
| file / converter / publisher / webhook / ai | 0 | 0 | 0 | 0 | 0 |
Free
| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |
|---|---|---|---|---|---|
| general | 100 | 500 | 5,000 | 10 | 5 |
| file | 100 | 500 | 5,000 | 10 | 5 |
| converter | 10 | 50 | 200 | 5 | 3 |
| publisher | 10 | 50 | 200 | 5 | 3 |
| webhook | 50 | 200 | 2,000 | 5 | 3 |
Included: 3 webhook subscriptions, 1 team member, 100 MB storage, 200 AI credits a month.
Pro
| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |
|---|---|---|---|---|---|
| general | 3,600 | 50,000 | 500,000 | 120 | 20 |
| file | 3,600 | 50,000 | 500,000 | 120 | 20 |
| converter | 100 | 500 | 5,000 | 10 | 5 |
| publisher | 100 | 500 | 5,000 | 10 | 5 |
| webhook | 1,000 | 10,000 | 100,000 | 50 | 10 |
Included: 25 webhook subscriptions, 10 team members, 5 GB storage, 2,000 AI credits a month.
Enterprise
| Category | Hourly | Daily | Monthly | Burst (req/min) | Burst capacity |
|---|---|---|---|---|---|
| general | 36,000 | 500,000 | 5,000,000 | 500 | 100 |
| file | 36,000 | 500,000 | 5,000,000 | 500 | 100 |
| converter | 1,000 | 5,000 | 50,000 | 50 | 20 |
| publisher | 1,000 | 5,000 | 50,000 | 50 | 20 |
| webhook | 10,000 | 100,000 | 1,000,000 | 200 | 50 |
Included: 100 webhook subscriptions, 50 team members, 50 GB storage, 20,000 AI credits a month, priority support.
- In the API — every response carries the effective limits and remaining quota in the
RateLimitandRateLimit-Policyheaders (see Rate Limit Headers below), and the usage endpoint returns every layer of every category at once. - In the Portal — a live view of consumption versus limit per category is available under Organizations → your organization → Usage in the Typograph Portal.
Limits may differ from the numbers above if custom overrides are in effect on your organization.
Usage Endpoint
GET /v1/rate-limits/organizations/{organizationId}/usage returns the limits and current usage of every layer, per category, for an organization you are a member of. It takes a user token with the subscription or subscription:read scope, as GET /v1/subscription/usage does; without either it answers 403 insufficient_scope. An app's token reaches only the organization it was granted for: another one answers 403 organization_not_bound (which organization a token reaches). The call itself counts against the general quota of the organization your client belongs to.
curl https://api.typograph.nl/v1/rate-limits/organizations/$ORG_ID/usage \
-H "Authorization: Bearer $USER_TOKEN"
{
"organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"tier": "pro",
"categories": {
"publisher": {
"burst": { "limit": 5, "remaining": 5, "used": 0 },
"hourly": { "limit": 100, "remaining": 87, "used": 13 },
"daily": { "limit": 500, "remaining": 450, "used": 50, "reset_at": "<ISO 8601 timestamp>" },
"monthly": { "limit": 5000, "remaining": 4000, "used": 1000, "reset_at": "<ISO 8601 timestamp>" }
}
}
}
A category your plan blocks is left out. The gateway answers this endpoint itself, with its own error codes: a client token gets a 403 with user_token_required (not the invalid_token_type of the proxied routes), an organization you are not a member of a 403 with not_organization_member.
Endpoint Categories
Rate limits are applied per category based on the endpoint path:
| Category | Endpoints | Description |
|---|---|---|
| general | /v1/identity/*, /oauth/*, /v1/document/*, /v1/rate-limits/*, / | Standard API calls |
| converter | /v1/converter/* | Format conversion (heavy resources) |
| publisher | /v1/publisher/* | PDF generation (heavy resources) |
| file | /v1/file/* | File and storage operations |
| webhook | /v1/webhook/* | Webhook management |
| ai | The portal's AI assistant | The assistant's requests; its routes are for the portal only, not part of the public API |
The ai category has the same limits on Free, Pro and Enterprise, not listed here since an integration never spends them, and no allowance at all for anonymous requests (0, as in the Anonymous Tier table). It is in the usage responses all the same: in the usage endpoint and in GET /v1/subscription/usage.
AI credits
What the portal's AI assistant does is metered in AI credits, a monthly allowance per organization: 500 on Free, 5,000 on Pro and 20,000 on Enterprise (quotas.ai_credits_monthly of the plan). Pro and Enterprise can add credit bundles of 5,000 or 15,000 credits to the current month. AI credits are not requests and have no rate limits, but an organization uses at most a quarter of its month's credits, bundles included, in a UTC day. GET /v1/subscription/usage reports them as ai_credits, in usage the credits used this month and in monthly_limits the credits the organization has, with any custom limit or add-on applied; 0 is no AI credits, never unlimited. They reset with the calendar month, like the monthly request limits. What the assistant spends them on, and its own limits: Assistant → AI Credits.
GET /v1/subscription/usage/ai-credits (subscription / subscription:read, a user token) shows them in more detail, for the organization named in the X-Typograph-On-Behalf-Of-Organization-Id header: the month and the day, and who used them.
{
"organization_id": "019b28fb-a11e-7641-a28f-e978f892ec01",
"period": "<YYYY-MM>",
"period_ends_at": "<ISO 8601 timestamp>",
"resets_at": "<ISO 8601 timestamp>",
"limit": 12000,
"used": 9900,
"left": 2100,
"daily_limit": 3000,
"daily_used": 900,
"daily_left": 2100,
"daily_resets_at": "<ISO 8601 timestamp>",
"people": [
{"user_id": "019b28fb-a11e-7641-a28f-e978f892ec06", "name": "Grace Hopper", "is_guest": false, "used": 5400},
{"user_id": "019b28fb-a11e-7641-a28f-e978f892ec07", "name": "Ada Lovelace", "is_guest": true, "used": 4500}
],
"alert": {"threshold": 80, "top_user_id": "019b28fb-a11e-7641-a28f-e978f892ec06", "top_user_name": "Grace Hopper", "top_used": 5400}
}
The owner and admins get everyone in people, guests included (is_guest: people with a role on the organization's teams who are not members); any other member gets only their own row. used is always the organization's total. alert is set once 80% of the month is used, and is null before that; for the owner and admins it names who used the most, for anyone else its top_user_id, top_user_name and top_used are null. A name is null when it is not known, for someone who has since left. resets_at and daily_resets_at are in UTC.
Rate Limit Layers
The API uses four layers of rate limiting:
Layer 1: Burst Protection (Token Bucket)
Prevents traffic spikes by limiting requests per minute with a burst capacity.
- How it works: Tokens refill continuously at the specified rate
- Burst capacity: Allows short bursts above the sustained rate
- Typical wait: Seconds when exceeded
Layer 2: Hourly Quota (Sliding Window)
Enforces the sustained hourly rate limit using a sliding window algorithm.
- Window: 1 hour rolling window
- Algorithm: Sliding window (no boundary burst issues)
- Typical wait: Minutes to an hour when exceeded
Layer 3: Daily Quota (Fixed Window)
Limits total requests per calendar day (UTC).
- Window: Resets at UTC midnight
- Available: Pro tier and above
- Typical wait: Hours when exceeded
Layer 4: Monthly Quota (Fixed Window)
Limits total requests per calendar month.
- Window: Resets on the 1st of each month (UTC)
- Available: Pro tier and above
- Typical wait: Days when exceeded
Rate Limit Headers
The API uses IETF-standard rate limit headers to communicate quota status.
Response Headers
| Header | Description | Format |
|---|---|---|
RateLimit | Current limit status | "category";r=remaining;t=reset_seconds |
RateLimit-Policy | Configured policies | Comma-separated policy definitions |
Retry-After | Seconds until retry (when limited) | Integer seconds |
Example Response Headers
HTTP/1.1 200 OK
Content-Type: application/json
RateLimit: "general";r=3542;t=1800
RateLimit-Policy: "general_hourly";q=3600;w=3600, "general_burst";q=120;w=60, "general_daily";q=50000;w=86400
X-Typograph-Request-Id: 019b28fb-a11e-7641-a28f-e978f892ec06
Header Format Explained
RateLimit Header:
"general"- The endpoint categoryr=3542- Remaining requests in current windowt=1800- Seconds until the window resets
RateLimit-Policy Header:
q- Quota (maximum requests allowed)w- Window size in seconds (3600 = 1 hour, 86400 = 1 day)
Rate Limit Exceeded
Hourly Limit Exceeded
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
RateLimit: "general";r=0;t=1800
Retry-After: 1800
{
"error": "rate_limit_exceeded",
"error_description": "Hourly rate limit exceeded for general endpoints. Limit: 3600 requests per hour."
}
Burst Limit Exceeded
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 5
{
"error": "burst_rate_limit_exceeded",
"error_description": "Burst rate limit exceeded for general endpoints. Too many requests in a short time period."
}
Daily Quota Exceeded
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 28800
{
"error": "daily_quota_exceeded",
"error_description": "Daily quota exceeded for converter endpoints. Limit: 2000 requests."
}
Monthly Quota Exceeded
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 172800
{
"error": "monthly_quota_exceeded",
"error_description": "Monthly quota exceeded for converter endpoints. Limit: 20000 requests."
}
Too Many Invalid Tokens
A request with an invalid or expired access token is answered with 401 invalid_token. An address that keeps sending them — more than 30 in a short burst, or more than 300 in an hour — gets 429 on every endpoint that takes a token, even with a valid token. A block lasts at most a minute, also once the hourly limit is spent; after it a valid token works again, while another invalid one blocks the address for the next minute until the hour allows more. Retry-After says how long to wait. An IPv6 address counts together with the rest of its /64 network. An integration that refreshes its token when it gets a 401 never comes near this limit.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 2
{
"error": "authentication_rate_limit_exceeded",
"error_description": "Too many requests with an invalid or expired token. Try again later."
}
Anonymous requests, such as token requests to POST /oauth/token, are counted per address the same way, against the Anonymous Tier.
Handling Rate Limits
Parse IETF Headers
function parseRateLimitHeader(response) {
const rateLimit = response.headers.get('RateLimit');
if (!rateLimit) return null;
// Parse: "category";r=remaining;t=reset_seconds
const match = rateLimit.match(/"([^"]+)";r=(\d+);t=(\d+)/);
if (!match) return null;
return {
category: match[1],
remaining: parseInt(match[2], 10),
resetSeconds: parseInt(match[3], 10)
};
}
async function makeRequest(url, options) {
const response = await fetch(url, options);
const limits = parseRateLimitHeader(response);
if (limits && limits.remaining < 100) {
console.warn(`Rate limit warning: ${limits.remaining} requests remaining for ${limits.category}`);
}
return response;
}
Implement Exponential Backoff
async function requestWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
const retryAfter = parseInt(response.headers.get('Retry-After'), 10) || 60;
const delay = Math.min(retryAfter * 1000, Math.pow(2, attempt) * 1000);
console.log(`Rate limited. Waiting ${delay}ms before retry...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}
Queue and Throttle Requests
For batch operations, queue requests and process them at a sustainable rate:
class RateLimitedQueue {
constructor(requestsPerSecond = 1) {
this.queue = [];
this.interval = 1000 / requestsPerSecond;
this.processing = false;
}
async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
const { fn, resolve, reject } = this.queue.shift();
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
setTimeout(() => {
this.processing = false;
this.process();
}, this.interval);
}
}
// Usage: Process at 1 request per second
const queue = new RateLimitedQueue(1);
const results = await Promise.all(
jobIds.map(id => queue.add(() => fetchJob(id)))
);
Best Practices
Cache Responses
Cache API responses to reduce the number of requests:
const cache = new Map();
const CACHE_TTL = 60000; // 1 minute
async function getCachedTeams(accessToken) {
const cacheKey = 'teams';
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const response = await fetch('https://api.typograph.nl/v1/file/teams', {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const data = await response.json();
cache.set(cacheKey, { data, timestamp: Date.now() });
return data;
}
Use Webhooks Instead of Polling
Instead of polling for job status, use webhooks to receive notifications:
// Instead of polling every second
setInterval(async () => {
const job = await getJob(jobId);
if (job.status === 'completed') {
// Handle completion
}
}, 1000);
// Use webhooks
app.post('/webhooks/typograph', (req, res) => {
const { event, job } = req.body;
if (job.status === 'completed') {
// Handle completion
}
res.status(200).json({ received: true });
});
Separate Clients by Category
If you have high usage in specific categories, consider using separate OAuth clients:
const clients = {
general: { clientId: '...', accessToken: '...' },
converter: { clientId: '...', accessToken: '...' },
publisher: { clientId: '...', accessToken: '...' }
};
async function convertDocument(file) {
return fetch('https://api.typograph.nl/v1/converter/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${clients.converter.accessToken}`
},
body: file
});
}
Avoid Traffic Spikes
To avoid hitting burst limits:
- Spread requests evenly - Don't send many requests at once
- Add small delays - Add 100-500ms delays between batch requests
- Use queuing - Process requests through a rate-limited queue
- Monitor headers - Watch
RateLimitremaining values closely
Error Codes Summary
| Error Code | Description | Typical Retry Time |
|---|---|---|
burst_rate_limit_exceeded | Too many requests in short period | Seconds |
rate_limit_exceeded | Hourly quota exceeded | Minutes to an hour |
daily_quota_exceeded | Daily quota exceeded | Hours |
monthly_quota_exceeded | Monthly quota exceeded | Days |
Upgrading Your Plan
Pro and Enterprise are on request: the API answers their price_monthly and price_yearly as null. A plan is not changed through the API.
- Pro: ask for it in the Typograph Portal, on your organization's Subscription page under Change plan, or mail sales@typograph.nl. Sales agrees the price with you and then moves your organization.
- Enterprise: contact sales@typograph.nl for pricing and custom rate-limit overrides.
Custom Rate Limits
Enterprise accounts can request custom rate limits per category. Contact support with:
- Your organization ID
- The categories that need custom limits
- Desired hourly / daily / monthly quotas
- Rationale for the increase
Approved overrides apply to every OAuth client in the organization and take effect within 5 minutes (the gateway's limit cache TTL).