Rate Limits by Tier
Rate limits are enforced per API key on a rolling 24-hour window starting from your first request of the day. Limits reset at midnight UTC.
Explorer
3 / day
3 requests per 24 hours
Free
Starter
25 / day
25 requests per 24 hours
$99/mo
Professional
100 / day
100 requests per 24 hours
$249/mo
Business
∞
Unlimited requests
$499/mo
Important Notes
The /health endpoint is not rate-limited. Bulk endpoints like /schools/bulk-import count as a single request regardless of the number of schools processed. AI endpoints (/agent/chat, /grants/write) have separate quotas that don't count against your daily API limit.
Rate Limit Headers
Every API response includes comprehensive rate limit information in the response headers to help you monitor your usage proactively.
| Header | Description | Example |
X-RateLimit-Limit | Maximum requests allowed per day for your tier | 100 |
X-RateLimit-Remaining | Requests remaining in the current 24-hour window | 73 |
X-RateLimit-Reset | Unix timestamp when the rate limit window resets (midnight UTC) | 1740528000 |
X-RateLimit-Used | Total requests used in the current window | 27 |
Retry-After | Seconds until you can retry (only present on 429 responses) | 3600 |
Pro Tip
Monitor the X-RateLimit-Remaining header in your applications to implement proactive throttling before hitting the limit. This prevents your requests from failing with 429 errors.
Handling 429 Too Many Requests
When you exceed your rate limit, the API returns a 429 status code with detailed error information and a Retry-After header indicating when you can retry.
429 Response Example
HTTP Response
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 25
X-RateLimit-Remaining: 0
X-RateLimit-Used: 25
X-RateLimit-Reset: 1740528000
Retry-After: 3600
{
"detail": "Rate limit exceeded. 25/25 requests used today. Resets at midnight UTC.",
"type": "rate_limit_exceeded",
"title": "Too Many Requests",
"status": 429,
"upgrade_url": "https://thinkkits.com/pricing",
"reset_time": "2026-02-25T00:00:00Z"
}
Important
Always respect the Retry-After header. Clients that repeatedly ignore rate limits may be temporarily blocked from accessing the API. Implement exponential backoff for resilient error handling.
Code Examples
Shell / curl
bash
# Check remaining quota in response headers
curl -si "https://api.thinkkits.com/schools/search?q=Springfield" \
-H "X-API-Key: tk_live_abc123" \
| grep -i "x-ratelimit"
# Output:
# X-RateLimit-Limit: 100
# X-RateLimit-Remaining: 99
# X-RateLimit-Used: 1
# X-RateLimit-Reset: 1740528000
JavaScript (fetch with retry logic)
JavaScript
class ThinkKitsAPI {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseURL = 'https://api.thinkkits.com';
}
async fetchWithRetry(endpoint, options = {}, maxRetries = 3) {
const url = `${this.baseURL}${endpoint}`;
const headers = {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers
};
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, { ...options, headers });
// Log rate limit info
const remaining = response.headers.get('X-RateLimit-Remaining');
const limit = response.headers.get('X-RateLimit-Limit');
console.log(`Rate limit: ${remaining}/${limit} remaining`);
if (response.status !== 429) {
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
return response;
}
// Handle rate limiting
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
const resetTime = new Date(response.headers.get('X-RateLimit-Reset') * 1000);
console.warn(`Rate limited. Retrying in ${retryAfter}s (resets at ${resetTime.toISOString()})...`);
if (attempt < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
}
}
throw new Error('Max retries exceeded for rate-limited request');
}
async searchSchools(query, limit = 10) {
const params = new URLSearchParams({ q: query, limit });
const response = await this.fetchWithRetry(`/schools/search?${params}`);
return response.json();
}
}
// Usage
const api = new ThinkKitsAPI('tk_live_abc123');
try {
const schools = await api.searchSchools('Springfield Elementary');
console.log('Found schools:', schools);
} catch (error) {
console.error('API request failed:', error.message);
}
Python (requests with exponential backoff)
Python
import time
import requests
from datetime import datetime
class ThinkKitsAPI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.thinkkits.com"
self.session = requests.Session()
self.session.headers.update({"X-API-Key": api_key})
def request_with_retry(self, method, endpoint, **kwargs):
url = f"{self.base_url}{endpoint}"
max_retries = kwargs.pop("max_retries", 3)
base_delay = kwargs.pop("base_delay", 1)
for attempt in range(max_retries):
response = self.session.request(method, url, **kwargs)
# Log rate limit info
remaining = response.headers.get("X-RateLimit-Remaining", "?")
limit = response.headers.get("X-RateLimit-Limit", "?")
print(f"Rate limit: {remaining}/{limit} remaining")
if response.status_code != 429:
response.raise_for_status()
return response
# Handle rate limiting with exponential backoff
retry_after = int(response.headers.get("Retry-After", 60))
reset_time = datetime.fromtimestamp(
int(response.headers.get("X-RateLimit-Reset", 0))
)
# Use exponential backoff, but respect Retry-After header
delay = max(retry_after, base_delay * (2 ** attempt))
print(f"Rate limited. Waiting {delay}s (resets at {reset_time})...")
if attempt < max_retries - 1:
time.sleep(delay)
raise requests.exceptions.RetryError(f"Max retries ({max_retries}) exceeded")
def search_schools(self, query, limit=10):
params = {"q": query, "limit": limit}
response = self.request_with_retry("GET", "/schools/search", params=params)
return response.json()
def get_usage_stats(self):
response = self.request_with_retry("GET", "/marketplace/usage")
return response.json()
# Usage
api = ThinkKitsAPI("tk_live_abc123")
try:
schools = api.search_schools("Springfield Elementary")
print(f"Found {len(schools['results'])} schools")
# Check usage stats
usage = api.get_usage_stats()
print(f"Daily usage: {usage['daily_used']}/{usage['daily_limit']}")
except requests.exceptions.RequestException as e:
print(f"API request failed: {e}")
Node.js (axios with interceptors)
Node.js
const axios = require('axios');
class ThinkKitsAPI {
constructor(apiKey) {
this.client = axios.create({
baseURL: 'https://api.thinkkits.com',
headers: { 'X-API-Key': apiKey },
timeout: 30000
});
// Add response interceptor for rate limit logging
this.client.interceptors.response.use(
(response) => {
const { headers } = response;
const remaining = headers['x-ratelimit-remaining'];
const limit = headers['x-ratelimit-limit'];
if (remaining && limit) {
console.log(`Rate limit: ${remaining}/${limit} remaining`);
}
return response;
},
async (error) => {
if (error.response?.status === 429) {
const retryAfter = parseInt(error.response.headers['retry-after']) || 60;
console.warn(`Rate limited. Auto-retrying in ${retryAfter}s...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
// Retry the original request
return this.client.request(error.config);
}
throw error;
}
);
}
async searchSchools(query, limit = 10) {
const response = await this.client.get('/schools/search', {
params: { q: query, limit }
});
return response.data;
}
}
// Usage
(async () => {
const api = new ThinkKitsAPI('tk_live_abc123');
try {
const schools = await api.searchSchools('Springfield Elementary');
console.log('Found schools:', schools.results.length);
} catch (error) {
console.error('API error:', error.message);
}
})();
Best Practices
- Monitor proactively. Check
X-RateLimit-Remaining headers in your application. Implement client-side throttling when approaching limits.
- Cache aggressively. School profiles and funding data change infrequently. Cache responses for 24 hours to reduce API calls and improve performance.
- Use bulk endpoints. The
/schools/bulk-import endpoint lets you process up to 100 schools in one request (Business tier), dramatically reducing your rate limit usage.
- Implement exponential backoff. When you receive a 429 error, wait the
Retry-After duration. For subsequent 429s, double your wait time to avoid overwhelming the API.
- Batch requests efficiently. Group related API calls together and implement request queuing to spread load across your rate limit window.
- Use webhooks instead of polling. Subscribe to customer webhooks to receive real-time notifications instead of repeatedly polling for changes.
- Upgrade when needed. If you consistently hit limits, upgrading tiers provides significant quota increases: Starter to Professional gives 4x capacity (25→100/day).
Enterprise Support
Business tier customers get priority API support and can request custom rate limits for high-volume integrations. Contact api@thinkkits.com for assistance.
Upgrading Your Tier
Need higher limits? Upgrade your plan through the billing portal, or use the API to check usage and upgrade programmatically.
API Usage
# Check current usage and limits
curl "https://api.thinkkits.com/marketplace/usage" \
-H "X-API-Key: tk_live_abc123"
# Response shows detailed usage breakdown
{
"tier": "STARTER",
"daily_limit": 25,
"daily_used": 18,
"daily_remaining": 7,
"reset_time": "2026-02-25T00:00:00Z",
"upgrade_url": "https://thinkkits.com/pricing"
}
You can also monitor usage patterns and set up alerts in your application:
JavaScript Monitoring
class RateLimitMonitor {
constructor(api, thresholds = { warning: 0.8, critical: 0.95 }) {
this.api = api;
this.thresholds = thresholds;
}
checkUsage(headers) {
const limit = parseInt(headers.get('X-RateLimit-Limit'));
const remaining = parseInt(headers.get('X-RateLimit-Remaining'));
const used = limit - remaining;
const usagePercent = used / limit;
if (usagePercent >= this.thresholds.critical) {
console.error(`🚨 Critical: ${Math.round(usagePercent * 100)}% of daily limit used`);
// Trigger upgrade flow or throttle requests
} else if (usagePercent >= this.thresholds.warning) {
console.warn(`⚠️ Warning: ${Math.round(usagePercent * 100)}% of daily limit used`);
// Suggest caching or optimization
}
return { used, limit, remaining, usagePercent };
}
}