> ## Documentation Index
> Fetch the complete documentation index at: https://docs.clemta.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate Limiting

> Understanding rate limits across all Clemta APIs

# Rate Limiting

Clemta APIs implement rate limiting to ensure fair usage and system stability. This page explains how rate limiting works across all our APIs.

## Rate Limit Types

Clemta APIs employ different rate limiting strategies depending on the endpoint:

1. **API Key-based Rate Limiting**: Applied to most endpoints
2. **Resource-based Rate Limiting**: Applied to specific resources (e.g., per company ID)

## Rate Limit Headers

All API responses include headers that provide information about rate limits:

### Common Headers

<ResponseField name="X-RateLimit-API" type="string">
  Displays your API key's rate limit (e.g., "100 requests per minute")
</ResponseField>

<ResponseField name="X-RateLimit-Remaining" type="integer">
  Number of requests remaining in the current time window
</ResponseField>

<ResponseField name="X-RateLimit-Limit" type="integer">
  Maximum number of requests allowed per minute
</ResponseField>

<ResponseField name="X-RateLimit-Reset" type="integer">
  Unix timestamp when the rate limit resets
</ResponseField>

<ResponseField name="X-RateLimit-Type" type="string">
  Indicates which rate limit type is being applied ("api\_key" or "resource")
</ResponseField>

When a rate limit is exceeded, you'll also receive:

<ResponseField name="Retry-After" type="integer">
  Seconds to wait before making another request (typically 60 seconds)
</ResponseField>

## Rate Limit Errors

When a rate limit is exceeded, the API will respond with:

* HTTP Status Code: `429 Too Many Requests`
* Error Code: `RATE_LIMIT_EXCEEDED`
* Error Message: "Rate limit exceeded"

```json theme={null}
{
  "success": false,
  "message": "Rate limit exceeded",
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "details": "Too many requests. Please try again later."
  }
}
```

## Best Practices

<Steps>
  <Step title="Implement exponential backoff">
    Use a retry strategy with exponential backoff when you receive 429.
  </Step>

  <Step title="Monitor your usage">
    Track `X-RateLimit-Remaining` and slow down before hitting limits.
  </Step>

  <Step title="Use conditional requests">
    Use `If-Modified-Since` for GET endpoints.
  </Step>

  <Step title="Use pagination">Paginate list endpoints to reduce load.</Step>
</Steps>

### 1. Implement Exponential Backoff

<CodeGroup dropdown>
  ```go exponential_backoff.go theme={null}
  package main

  import (
  	"fmt"
  	"math"
  	"net/http"
  	"strconv"
  	"time"
  )

  func makeRequestWithRetry(url string, apiKey string, maxRetries int) (*http.Response, error) {
  	client := &http.Client{}
  	for attempt := 0; attempt < maxRetries; attempt++ {
  		req, err := http.NewRequest("GET", url, nil)
  		if err != nil {
  			return nil, err
  		}
  		req.Header.Set("X-API-KEY", apiKey)

  		resp, err := client.Do(req)
  		if err != nil {
  			if attempt == maxRetries-1 {
  				return nil, err
  			}
  			continue
  		}

  		if resp.StatusCode == http.StatusTooManyRequests { // 429
  			retryAfter := resp.Header.Get("Retry-After")
  			var delay time.Duration
  			if retryAfter != "" {
  				if seconds, err := strconv.Atoi(retryAfter); err == nil {
  					delay = time.Duration(seconds) * time.Second
  				}
  			}
  			if delay == 0 {
  				ms := math.Min(1000*math.Pow(2, float64(attempt)), 60000)
  				delay = time.Duration(ms) * time.Millisecond
  			}
  			fmt.Printf("Rate limited. Waiting %v before retry %d\n", delay, attempt+1)
  			time.Sleep(delay)
  			continue
  		}

  		return resp, nil
  	}
  	return nil, fmt.Errorf("max retries exceeded")
  }
  ```

  ```typescript exponential_backoff.ts theme={null}
  export async function makeRequestWithRetry(
    url: string,
    options: RequestInit,
    maxRetries: number = 3
  ): Promise<Response> {
    let attempt = 0;
    while (attempt < maxRetries) {
      try {
        const response = await fetch(url, options);
        if (response.status === 429) {
          const retryAfter = response.headers.get("Retry-After");
          const delay = retryAfter
            ? parseInt(retryAfter, 10) * 1000
            : Math.min(1000 * Math.pow(2, attempt), 60_000);
          console.log(
            `Rate limited. Waiting ${delay}ms before retry ${attempt + 1}`
          );
          await new Promise((resolve) => setTimeout(resolve, delay));
          attempt += 1;
          continue;
        }
        return response;
      } catch (error) {
        if (attempt === maxRetries - 1) {
          throw error;
        }
        attempt += 1;
      }
    }
    throw new Error("Max retries exceeded");
  }

  // Usage
  await makeRequestWithRetry("https://api.clemta.com/formations/123", {
    headers: { "X-API-KEY": "your_api_key" },
  });
  ```

  ```rust exponential_backoff.rs theme={null}
  use reqwest::Client;
  use std::time::Duration;
  use tokio::time::sleep;

  pub async fn make_request_with_retry(
      url: &str,
      api_key: &str,
      max_retries: u32,
  ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
      let client = Client::new();
      let mut attempt: u32 = 0;

      while attempt < max_retries {
          let response = client
              .get(url)
              .header("X-API-KEY", api_key)
              .send()
              .await?;

          if response.status() == 429 {
              let retry_after = response
                  .headers()
                  .get("Retry-After")
                  .and_then(|h| h.to_str().ok())
                  .and_then(|s| s.parse::<u64>().ok());

              let delay = retry_after
                  .map(Duration::from_secs)
                  .unwrap_or_else(|| {
                      let ms = (1000.0_f64 * 2.0_f64.powi(attempt as i32)).min(60000.0);
                      Duration::from_millis(ms as u64)
                  });

              println!(
                  "Rate limited. Waiting {:?} before retry {}",
                  delay,
                  attempt + 1
              );
              sleep(delay).await;
              attempt += 1;
              continue;
          }

          return Ok(response);
      }

      Err("Max retries exceeded".into())
  }
  ```

  ```java ExponentialBackoff.java theme={null}
  import java.io.IOException;
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.net.URI;
  import java.util.Optional;

  public class ExponentialBackoff {
    private static final HttpClient client = HttpClient.newHttpClient();

    public static HttpResponse<String> makeRequestWithRetry(
        String url, String apiKey, int maxRetries) throws IOException, InterruptedException {
      for (int attempt = 0; attempt < maxRetries; attempt++) {
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("X-API-KEY", apiKey)
            .GET()
            .build();

        HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() == 429) {
          Optional<String> retryAfter = res.headers().firstValue("Retry-After");
          long delayMs = retryAfter
              .map(s -> Long.parseLong(s) * 1000)
              .orElse(Math.min((long) Math.pow(2, attempt) * 1000L, 60_000L));
          System.out.printf("Rate limited. Waiting %dms before retry %d%n", delayMs, attempt + 1);
          Thread.sleep(delayMs);
          continue;
        }

        return res;
      }

      throw new RuntimeException("Max retries exceeded");
    }
  }
  ```

  ```python exponential_backoff.py theme={null}
  import asyncio
  import math
  from typing import Optional

  import aiohttp


  async def make_request_with_retry(
      url: str, api_key: str, max_retries: int = 3
  ) -> aiohttp.ClientResponse:
      async with aiohttp.ClientSession() as session:
          for attempt in range(max_retries):
              try:
                  headers = {"X-API-KEY": api_key}
                  async with session.get(url, headers=headers) as resp:
                      if resp.status == 429:
                          retry_after = resp.headers.get("Retry-After")
                          delay_ms: Optional[float] = (
                              int(retry_after) * 1000 if retry_after else None
                          )
                          if delay_ms is None:
                              delay_ms = min(1000 * math.pow(2, attempt), 60000)
                          print(
                              f"Rate limited. Waiting {int(delay_ms)}ms before retry {attempt + 1}"
                          )
                          await asyncio.sleep(delay_ms / 1000)
                          continue
                      return resp
              except Exception:
                  if attempt == max_retries - 1:
                      raise
          raise Exception("Max retries exceeded")
  ```
</CodeGroup>

### 2. Monitor Rate Limit Headers

<CodeGroup dropdown>
  ```go monitor_headers.go theme={null}
  package main

  import (
  	"fmt"
  	"net/http"
  	"strconv"
  	"time"
  )

  func makeRequestWithMonitoring(url string, apiKey string) (*http.Response, error) {
  	client := &http.Client{}
  	req, err := http.NewRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	req.Header.Set("X-API-KEY", apiKey)

  	resp, err := client.Do(req)
  	if err != nil {
  		return nil, err
  	}

  	remaining := resp.Header.Get("X-RateLimit-Remaining")
  	reset := resp.Header.Get("X-RateLimit-Reset")
  	limit := resp.Header.Get("X-RateLimit-Limit")
  	fmt.Printf("Rate limit: %s/%s remaining\n", remaining, limit)

  	if remaining != "" && reset != "" {
  		if r, err := strconv.Atoi(remaining); err == nil && r < 10 {
  			if rs, err := strconv.ParseInt(reset, 10, 64); err == nil {
  				wait := (rs * 1000) - time.Now().UnixMilli()
  				if wait > 0 {
  					fmt.Printf("Waiting %dms before next request\n", wait)
  					time.Sleep(time.Duration(wait) * time.Millisecond)
  				}
  			}
  		}
  	}
  	return resp, nil
  }
  ```

  ```typescript monitor_headers.ts theme={null}
  export async function makeRequestWithMonitoring(
    url: string,
    options: RequestInit
  ): Promise<Response> {
    const response = await fetch(url, options);
    const remaining = response.headers.get("X-RateLimit-Remaining");
    const reset = response.headers.get("X-RateLimit-Reset");
    const limit = response.headers.get("X-RateLimit-Limit");

    console.log(`Rate limit: ${remaining}/${limit} remaining`);

    if (remaining && reset) {
      const r = parseInt(remaining, 10);
      if (r < 10) {
        const waitTime = parseInt(reset, 10) * 1000 - Date.now();
        if (waitTime > 0) {
          console.log(`Waiting ${waitTime}ms before next request`);
          await new Promise((resolve) => setTimeout(resolve, waitTime));
        }
      }
    }

    return response;
  }
  ```

  ```rust monitor_headers.rs theme={null}
  use reqwest::Client;
  use std::time::{Duration, SystemTime, UNIX_EPOCH};

  pub async fn make_request_with_monitoring(
      url: &str,
      api_key: &str,
  ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
      let client = Client::new();
      let response = client
          .get(url)
          .header("X-API-KEY", api_key)
          .send()
          .await?;

      let remaining = response
          .headers()
          .get("X-RateLimit-Remaining")
          .and_then(|h| h.to_str().ok());
      let reset = response
          .headers()
          .get("X-RateLimit-Reset")
          .and_then(|h| h.to_str().ok());
      let limit = response
          .headers()
          .get("X-RateLimit-Limit")
          .and_then(|h| h.to_str().ok());

      println!(
          "Rate limit: {}/{} remaining",
          remaining.unwrap_or("?"),
          limit.unwrap_or("?")
      );

      if let (Some(r), Some(rs)) = (remaining, reset) {
          if let (Ok(ri), Ok(rsi)) = (r.parse::<i32>(), rs.parse::<u64>()) {
              if ri < 10 {
                  let now = SystemTime::now()
                      .duration_since(UNIX_EPOCH)
                      .unwrap()
                      .as_millis() as u64;
                  let wait = (rsi * 1000).saturating_sub(now);
                  if wait > 0 {
                      println!("Waiting {}ms before next request", wait);
                      tokio::time::sleep(Duration::from_millis(wait)).await;
                  }
              }
          }
      }

      Ok(response)
  }
  ```

  ```java MonitorHeaders.java theme={null}
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.net.URI;
  import java.time.Instant;

  public class MonitorHeaders {
    private static final HttpClient client = HttpClient.newHttpClient();

    public static HttpResponse<String> makeRequestWithMonitoring(
        String url, String apiKey) throws Exception {
      HttpRequest req = HttpRequest.newBuilder()
          .uri(URI.create(url))
          .header("X-API-KEY", apiKey)
          .GET()
          .build();

      HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
      String remaining = res.headers().firstValue("X-RateLimit-Remaining").orElse("?");
      String reset = res.headers().firstValue("X-RateLimit-Reset").orElse("?");
      String limit = res.headers().firstValue("X-RateLimit-Limit").orElse("?");
      System.out.printf("Rate limit: %s/%s remaining%n", remaining, limit);

      if (!"?".equals(remaining) && !"?".equals(reset)) {
        int ri = Integer.parseInt(remaining);
        if (ri < 10) {
          long wait = (Long.parseLong(reset) * 1000) - Instant.now().toEpochMilli();
          if (wait > 0) {
            System.out.printf("Waiting %dms before next request%n", wait);
            Thread.sleep(wait);
          }
        }
      }

      return res;
    }
  }
  ```

  ```python monitor_headers.py theme={null}
  import asyncio
  import time

  import aiohttp


  async def make_request_with_monitoring(url: str, api_key: str):
      async with aiohttp.ClientSession() as session:
          headers = {"X-API-KEY": api_key}
          async with session.get(url, headers=headers) as resp:
              remaining = resp.headers.get("X-RateLimit-Remaining", "?")
              reset = resp.headers.get("X-RateLimit-Reset", "?")
              limit = resp.headers.get("X-RateLimit-Limit", "?")
              print(f"Rate limit: {remaining}/{limit} remaining")
              if remaining != "?" and reset != "?":
                  if int(remaining) < 10:
                      wait = (int(reset) * 1000) - int(time.time() * 1000)
                      if wait > 0:
                          print(f"Waiting {wait}ms before next request")
                          await asyncio.sleep(wait / 1000)
              return resp
  ```
</CodeGroup>

### 3. Use Conditional Requests

<CodeGroup dropdown>
  ```go conditional_requests.go theme={null}
  package main

  import (
  	"fmt"
  	"net/http"
  )

  func makeConditionalRequest(url string, apiKey string, lastModified string) (*http.Response, error) {
  	client := &http.Client{}
  	req, err := http.NewRequest("GET", url, nil)
  	if err != nil {
  		return nil, err
  	}
  	req.Header.Set("X-API-KEY", apiKey)
  	if lastModified != "" {
  		req.Header.Set("If-Modified-Since", lastModified)
  	}

  	resp, err := client.Do(req)
  	if err != nil {
  		return nil, err
  	}

  	if resp.StatusCode == http.StatusNotModified { // 304
  		fmt.Println("No changes since last request")
  	} else {
  		fmt.Printf("Last-Modified: %s\n", resp.Header.Get("Last-Modified"))
  	}
  	return resp, nil
  }
  ```

  ```typescript conditional_requests.ts theme={null}
  export async function makeConditionalRequest(
    url: string,
    apiKey: string
  ): Promise<Response> {
    const lastModified = localStorage.getItem("lastModified");
    const headers: Record<string, string> = { "X-API-KEY": apiKey };
    if (lastModified) headers["If-Modified-Since"] = lastModified;

    const response = await fetch(url, { headers });

    if (response.status === 304) {
      console.log("No changes since last request");
    } else {
      const newLastModified = response.headers.get("Last-Modified");
      if (newLastModified) localStorage.setItem("Last-Modified", newLastModified);
    }

    return response;
  }
  ```

  ```rust conditional_requests.rs theme={null}
  use reqwest::header::HeaderMap;
  use reqwest::Client;

  pub async fn make_conditional_request(
      url: &str,
      api_key: &str,
      last_modified: Option<&str>,
  ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
      let client = Client::new();

      let mut headers = HeaderMap::new();
      headers.insert("X-API-KEY", api_key.parse()?);
      if let Some(lm) = last_modified {
          headers.insert("If-Modified-Since", lm.parse()?);
      }

      let res = client.get(url).headers(headers).send().await?;

      if res.status() == 304 {
          println!("No changes since last request");
      } else if let Some(lm) = res.headers().get("Last-Modified") {
          println!("Last-Modified: {}", lm.to_str()?);
      }

      Ok(res)
  }
  ```

  ```java ConditionalRequests.java theme={null}
  import java.net.http.HttpClient;
  import java.net.http.HttpRequest;
  import java.net.http.HttpResponse;
  import java.net.URI;
  import java.util.Optional;

  public class ConditionalRequests {
    private static final HttpClient client = HttpClient.newHttpClient();

    public static HttpResponse<String> makeConditionalRequest(
        String url, String apiKey, Optional<String> lastModified) throws Exception {
      HttpRequest.Builder b = HttpRequest.newBuilder()
          .uri(URI.create(url))
          .header("X-API-KEY", apiKey)
          .GET();
      lastModified.ifPresent(lm -> b.header("If-Modified-Since", lm));

      HttpResponse<String> res = client.send(b.build(), HttpResponse.BodyHandlers.ofString());
      if (res.statusCode() == 304) {
        System.out.println("No changes since last request");
      } else {
        res.headers().firstValue("Last-Modified")
            .ifPresent(lm -> System.out.println("Last-Modified: " + lm));
      }
      return res;
    }
  }
  ```

  ```python conditional_requests.py theme={null}
  import aiohttp
  from typing import Optional


  async def make_conditional_request(
      url: str, api_key: str, last_modified: Optional[str] = None
  ):
      async with aiohttp.ClientSession() as session:
          headers = {"X-API-KEY": api_key}
          if last_modified:
              headers["If-Modified-Since"] = last_modified
          async with session.get(url, headers=headers) as resp:
              if resp.status == 304:
                  print("No changes since last request")
              else:
                  lm = resp.headers.get("Last-Modified")
                  if lm:
                      print(f"Last-Modified: {lm}")
              return resp
  ```
</CodeGroup>

## Example Rate Limit Headers

### Successful Request

```
X-RateLimit-API: 100 requests per minute
X-RateLimit-Remaining: 95
X-RateLimit-Limit: 100
X-RateLimit-Reset: 1640995200
X-RateLimit-Type: api_key
```

### Rate Limited Request

```
X-RateLimit-API: 100 requests per minute
X-RateLimit-Remaining: 0
X-RateLimit-Limit: 100
X-RateLimit-Reset: 1640995200
X-RateLimit-Type: api_key
Retry-After: 60
```
