Rate Limits
The VisibilityIQ API enforces per-endpoint rate limits to protect platform stability. Limits are based on the number of requests per time window, with some endpoints also metered on data rows returned.
Current limits
Section titled “Current limits”| Endpoint | Requests / minute | Row limit | Notes |
|---|---|---|---|
GET /v1/projects | 60 | — | |
GET /v1/projects/:id | 60 | — | |
GET /v1/projects/:id/audits | 30 | — | |
GET /v1/projects/:id/issues | 30 | 1,000/request | Use limit and offset to paginate |
GET /v1/backlinks/:target | 10 | 1,000/request | Credit-metered; see below |
POST /v1/backlinks/batch | 5 | 25 targets | 10 credits per target |
GET /v1/projects/:id/rankings | 30 | 500/request |
Credit metering
Section titled “Credit metering”Backlink endpoints are both rate-limited and credit-metered. Each GET /v1/backlinks/:target call costs a base of 10 credits plus 1 credit per row returned. POST /v1/backlinks/batch costs 10 credits per unique target, regardless of limitPerTarget.
Credits are deducted from your monthly data budget. Check your remaining balance under Account → Billing → Data usage.
Rate limit headers
Section titled “Rate limit headers”Every response includes rate limit headers:
X-RateLimit-Limit: 10X-RateLimit-Remaining: 7X-RateLimit-Reset: 1750012345X-RateLimit-Limit— the total requests allowed in the current windowX-RateLimit-Remaining— requests remaining in the current windowX-RateLimit-Reset— Unix timestamp when the window resets
Handling 429 responses
Section titled “Handling 429 responses”When you exceed the rate limit, the API returns:
HTTP/1.1 429 Too Many RequestsRetry-After: 42Content-Type: application/json
{ "error": "Rate limit exceeded", "status": 429}The Retry-After header tells you how many seconds to wait before retrying. Do not retry immediately — that will keep returning 429 and waste time.
Recommended handling in code:
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const res = await fetch(url, options);
if (res.status === 429) { const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60', 10); await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); continue; }
return res; } throw new Error('Max retries exceeded');}Staying within limits
Section titled “Staying within limits”- Cache responses when the underlying data doesn’t change between requests (audit findings, for example, only change when a new audit runs)
- Use pagination to fetch large result sets across multiple requests rather than requesting all rows at once
- For backlink data, batch multiple targets in a single
POST /v1/backlinks/batchcall rather than multiple individual GET requests - Implement exponential backoff with jitter if you’re making high-volume requests