Rate limits & throttling


⚠️

Now generally available

Rate 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 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');
}