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
  4. On the Free plan, converter and publisher work also counts per person - see below
  5. 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 ai category

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 409 and free_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).

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 / webhook / ai00000

Free​

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

Included: 3 webhook subscriptions, 1 team member, 100 MB storage, 200 AI credits a month.

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, 2,000 AI credits a month.

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, 20,000 AI credits a month, 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), 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:

CategoryEndpointsDescription
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
aiThe portal's AI assistantThe 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​

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."
}

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:

  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 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:

  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).