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
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.
| 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 | 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.
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.
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, priority support.
- In the API — every response carries the effective limits and remaining quota in the
RateLimitandRateLimit-Policyheaders (see Rate Limit Headers below). - 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.
Endpoint Categories
Rate limits are applied per category based on the endpoint path:
| Category | Endpoints | Description |
|---|---|---|
| general | /v1/identity/*, /oauth/*, /v1/document/* | 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 |
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."
}
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: upgrade from Free through the Typograph Portal under your organization's subscription settings.
- 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@typograph.nl 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).