Skip to main content

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

  1. Every OAuth client belongs to an organization - When you create an OAuth client, it's associated with your organization
  2. Organization subscription determines limits - Your organization's plan (Free, Pro, Enterprise) determines the rate limits
  3. 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.

PlanUse Case
AnonymousUnauthenticated requests, restricted to general category
FreeDevelopment and low-volume integrations
ProStandard production workloads
EnterpriseHigh-volume production
Anonymous Access

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.

CategoryHourlyDailyMonthlyBurst (req/min)Burst capacity
general1001,00010,0003020
file / converter / publisher / webhook00000

Free

CategoryHourlyDailyMonthlyBurst (req/min)Burst capacity
general1005005,000105
file1005005,000105
converter105020053
publisher105020053
webhook502002,00053

Included: 3 webhook subscriptions, 1 team member, 100 MB storage.

Pro

CategoryHourlyDailyMonthlyBurst (req/min)Burst capacity
general3,60050,000500,00012020
file3,60050,000500,00012020
converter1005005,000105
publisher1005005,000105
webhook1,00010,000100,0005010

Included: 25 webhook subscriptions, 10 team members, 5 GB storage.

Enterprise

CategoryHourlyDailyMonthlyBurst (req/min)Burst capacity
general36,000500,0005,000,000500100
file36,000500,0005,000,000500100
converter1,0005,00050,0005020
publisher1,0005,00050,0005020
webhook10,000100,0001,000,00020050

Included: 100 webhook subscriptions, 50 team members, 50 GB storage, priority support.

Your live limits and current usage
  • In the API — every response carries the effective limits and remaining quota in the RateLimit and RateLimit-Policy headers (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:

CategoryEndpointsDescription
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

HeaderDescriptionFormat
RateLimitCurrent limit status"category";r=remaining;t=reset_seconds
RateLimit-PolicyConfigured policiesComma-separated policy definitions
Retry-AfterSeconds 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 category
  • r=3542 - Remaining requests in current window
  • t=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:

  1. Spread requests evenly - Don't send many requests at once
  2. Add small delays - Add 100-500ms delays between batch requests
  3. Use queuing - Process requests through a rate-limited queue
  4. Monitor headers - Watch RateLimit remaining values closely

Error Codes Summary

Error CodeDescriptionTypical Retry Time
burst_rate_limit_exceededToo many requests in short periodSeconds
rate_limit_exceededHourly quota exceededMinutes to an hour
daily_quota_exceededDaily quota exceededHours
monthly_quota_exceededMonthly quota exceededDays

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:

  1. Your organization ID
  2. The categories that need custom limits
  3. Desired hourly / daily / monthly quotas
  4. 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).