Rate limits & throttling


⚠️

Beta feature – limited availability

Rate limiting is currently in beta and is only active for a selected group of accounts. If you are unsure whether your account has this feature enabled, contact your JOOR integration representative before building any throttling logic.


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 typeCredit 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-Id header).

Practical examples

ScenarioCredits usedResult
300 GET requests in 5 minutes300Quota exhausted
60 POST requests in 5 minutes300Quota exhausted
1 bulk POST + 295 GET requests300Quota exhausted
50 GET + 10 POST requests100200 credits remaining

Rate limit details in HTTP headers

Every API response includes headers to help you monitor your consumption in real time:

HeaderSample valueDescription
X-Throttling-Requested-Query-Cost1 or 5The credit cost of the current request.
X-Throttling-Credit-Available-In-Timeframe300Maximum credits allowed per window.
X-Throttling-Credit-Currently-Available299Remaining credits in the current window.
X-Throttling-Timeframe-In-Seconds300Duration of your window in seconds.
X-Throttling-Resets-At1751374233Unix timestamp of when your quota resets.
X-Throttling-Resets-In299Seconds remaining until your quota resets.
X-Throttling-Account-Id33888Your 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

  1. 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.
  2. 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.
  3. Monitor headers dynamically: Do not rely on fixed timers or assumptions. Read X-Throttling-Resets-In from each response to calculate the exact wait time needed — no more, no less.
  4. 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.
  5. Track remaining credits proactively: If X-Throttling-Credit-Currently-Available drops 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');
}