Rate limits & throttling
Now generally availableRate limiting is now part of the standard JOOR API experience:
- Sandbox environment: enabled for all accounts.
- Production environment: enabled by default for integrations created on or after May 19, 2026. Existing integrations will be activated progressively, with prior notice from your JOOR integration representative.
We recommend building throttling-aware logic into any new integration from day one.
To ensure platform stability and fair access for all users, the JOOR API enforces credit-based rate limits. When a limit is exceeded, the API returns an HTTP 429 Too Many Requests error.
Why do we have rate limits?
- Protection against abuse: Prevents service disruption from malicious actors or accidental traffic floods.
- Fair access: Ensures that excessive usage by one organization does not degrade performance for others.
- Load management: Helps JOOR manage traffic spikes and maintain a consistent experience.
- Prevention of incorrect usage: Discourages inefficient patterns like constant polling or sending individual updates instead of using bulk requests.
How it works: credit-based system
Instead of counting individual requests, we use credits to reflect the actual processing load of each operation:
| Request type | Credit cost |
|---|---|
| GET (Read) | 1 credit |
| POST / PUT / DELETE (Write) | 5 credits |
Default quotas
- Time frame: 300 seconds (5 minutes).
- Allowed credits: 300 credits per window.
- Window start: The 300-second window starts on your first request. It is a fixed window, not a sliding one — credits replenish fully at the start of the next window regardless of how many requests you made during the previous one.
- Scope: Rate limiting is applied per account (see
X-Throttling-Account-Idheader).
Practical examples
| Scenario | Credits used | Result |
|---|---|---|
| 300 GET requests in 5 minutes | 300 | Quota exhausted |
| 60 POST requests in 5 minutes | 300 | Quota exhausted |
| 1 bulk POST + 295 GET requests | 300 | Quota exhausted |
| 50 GET + 10 POST requests | 100 | 200 credits remaining |
Rate limit details in HTTP headers
Every API response includes headers to help you monitor your consumption in real time:
| Header | Sample value | Description |
|---|---|---|
X-Throttling-Requested-Query-Cost | 1 or 5 | The credit cost of the current request. |
X-Throttling-Credit-Available-In-Timeframe | 300 | Maximum credits allowed per window. |
X-Throttling-Credit-Currently-Available | 299 | Remaining credits in the current window. |
X-Throttling-Timeframe-In-Seconds | 300 | Duration of your window in seconds. |
X-Throttling-Resets-At | 1751374233 | Unix timestamp of when your quota resets. |
X-Throttling-Resets-In | 299 | Seconds remaining until your quota resets. |
X-Throttling-Account-Id | 33888 | Your account ID (throttling is per account). |
HTTP 429 response
When you exceed your quota, the API returns an HTTP 429 status. The response body follows this structure:
{
"data": [],
"errors": [
{
"message": "Too many requests",
"status": "TOO_MANY_REQUESTS",
"details": {
"details": "Request was throttled. Expected available in 222 second(s)."
}
}
]
}Use the X-Throttling-Resets-In header to determine how long to wait before retrying.
Best practices
- Use bulk endpoints: Sending one bulk POST (5 credits) is significantly more efficient than 100 individual POSTs (500 credits). Always prefer bulk operations for writes.
- Distribute your requests: Avoid exhausting your 300-credit quota in the first few seconds. Spreading calls evenly over the 5-minute window prevents spikes and ensures a smoother data flow.
- Monitor headers dynamically: Do not rely on fixed timers or assumptions. Read
X-Throttling-Resets-Infrom each response to calculate the exact wait time needed — no more, no less. - Implement smart retries: Always use incremental backoff and set a retry limit (e.g., 3 attempts). This prevents your application from entering infinite loops or crashing during sustained high activity.
- Track remaining credits proactively: If
X-Throttling-Credit-Currently-Availabledrops close to zero, pause before hitting the limit rather than waiting for a 429.
Implementation examples
GET request with retry logic
import requests
import time
def fetch_joor_data(url, headers, retries=3):
for attempt in range(retries):
response = requests.get(url, headers=headers)
if response.status_code == 429:
# Use the header value, with a safe fallback if it's missing
wait_time = int(response.headers.get("X-Throttling-Resets-In", 30))
print(f"Rate limit reached. Retrying in {wait_time + 1}s (Attempt {attempt + 1}/{retries})...")
time.sleep(wait_time + 1)
continue
response.raise_for_status()
return response.json()
raise Exception("Maximum retry attempts reached")const axios = require('axios');
async function fetchJoorData(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
if (error.response?.status === 429 && i < retries - 1) {
const waitTime = parseInt(error.response.headers['x-throttling-resets-in']) || Math.pow(2, i);
console.log(`Rate limit reached. Retrying in ${waitTime + 1}s (Attempt ${i + 1}/${retries})...`);
await new Promise(resolve => setTimeout(resolve, (waitTime + 1) * 1000));
continue;
}
throw error;
}
}
}Bulk POST with proactive credit monitoring
import requests
import time
BASE_URL = "https://api.joor.com/v4"
CREDIT_SAFETY_THRESHOLD = 10 # Pause if fewer than 10 credits remain
def bulk_post_with_throttling(endpoint, payload, headers, retries=3):
url = f"{BASE_URL}{endpoint}"
for attempt in range(retries):
response = requests.post(url, json=payload, headers=headers)
# Proactively check remaining credits before the next call
credits_remaining = int(response.headers.get("X-Throttling-Credit-Currently-Available", 999))
resets_in = int(response.headers.get("X-Throttling-Resets-In", 0))
if response.status_code == 429:
wait_time = int(response.headers.get("X-Throttling-Resets-In", 30))
print(f"Rate limit hit. Waiting {wait_time + 1}s (Attempt {attempt + 1}/{retries})...")
time.sleep(wait_time + 1)
continue
response.raise_for_status()
if credits_remaining < CREDIT_SAFETY_THRESHOLD:
print(f"Low credits ({credits_remaining} left). Pausing {resets_in + 1}s for window reset...")
time.sleep(resets_in + 1)
return response.json()
raise Exception("Maximum retry attempts reached")const axios = require('axios');
const CREDIT_SAFETY_THRESHOLD = 10; // Pause if fewer than 10 credits remain
async function bulkPostWithThrottling(url, payload, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await axios.post(url, payload);
const headers = response.headers;
const creditsRemaining = parseInt(headers['x-throttling-credit-currently-available']) ?? 999;
const resetsIn = parseInt(headers['x-throttling-resets-in']) ?? 0;
// Proactively pause if credits are running low
if (creditsRemaining < CREDIT_SAFETY_THRESHOLD) {
console.log(`Low credits (${creditsRemaining} left). Pausing ${resetsIn + 1}s for window reset...`);
await new Promise(resolve => setTimeout(resolve, (resetsIn + 1) * 1000));
}
return response.data;
} catch (error) {
if (error.response?.status === 429 && i < retries - 1) {
const waitTime = parseInt(error.response.headers['x-throttling-resets-in']) || Math.pow(2, i);
console.log(`Rate limit hit. Retrying in ${waitTime + 1}s (Attempt ${i + 1}/${retries})...`);
await new Promise(resolve => setTimeout(resolve, (waitTime + 1) * 1000));
continue;
}
throw error;
}
}
throw new Error('Maximum retry attempts reached');
}Updated 5 days ago
