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

# Rate Limiting

The Dakota Platform API implements rate limiting to ensure fair usage and maintain API performance for all clients.

## Rate Limits

API requests are rate limited based on authentication type:

| Authentication Type   | Rate Limit   | Window     |
| --------------------- | ------------ | ---------- |
| API Key (`x-api-key`) | 60 requests  | per minute |
| JWT (Dashboard users) | 600 requests | per minute |
| Unauthenticated       | 10 requests  | per minute |
| Application Token     | 250 requests | per hour   |

## Rate Limit Headers

Every API response includes rate limit information in the headers:

| Header                  | Description                                               |
| ----------------------- | --------------------------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests allowed in the current one-minute window |
| `X-RateLimit-Remaining` | Requests remaining in the current window                  |
| `X-RateLimit-Reset`     | Unix timestamp when the current rate-limit window resets  |

When a request is throttled (`429`), responses also include `Retry-After` with seconds to wait before retrying.

### Example Response Headers

```http theme={null}
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1640995260
Content-Type: application/json
```

## 429 Rate Limit Exceeded

When you exceed the rate limit, you'll receive a `429 Too Many Requests` response with RFC 9457 Problem Details:

```http theme={null}
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995290
Retry-After: 30
Content-Type: application/problem+json

{
  "type": "https://docs.dakota.xyz/errors/rate-limited",
  "title": "Rate Limited",
  "status": 429,
  "detail": "Too many requests. Please retry later.",
  "instance": "/customers",
  "request_id": "req_xyz789"
}
```

The `Retry-After` header indicates the number of seconds to wait before retrying.

## Handling Rate Limits

### 1. Monitor Rate Limit Headers

Always check the rate limit headers in your responses to avoid hitting limits:

```javascript JavaScript theme={null}
const response = await fetch('https://api.platform.dakota.xyz/customers', {
  headers: {
    'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='
  }
});

const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
const limit = parseInt(response.headers.get('X-RateLimit-Limit'));

// Calculate percentage of rate limit used
const usedPercentage = ((limit - remaining) / limit) * 100;

if (remaining < 50) {
  console.warn(`Rate limit critical: ${remaining}/${limit} requests remaining (${usedPercentage.toFixed(1)}% used)`);
} else if (remaining < 100) {
  console.info(`Rate limit warning: ${remaining}/${limit} requests remaining (${usedPercentage.toFixed(1)}% used)`);
}

// Calculate time until reset
const secondsUntilReset = resetTime - Math.floor(Date.now() / 1000);
if (secondsUntilReset > 0) {
  console.log(`Rate limit resets in ${secondsUntilReset} seconds`);
}
```

```python Python theme={null}
import requests
import time
from datetime import datetime

response = requests.get(
    'https://api.platform.dakota.xyz/customers',
    headers={'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='}
)

remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
limit = int(response.headers.get('X-RateLimit-Limit', 1000))

# Calculate percentage of rate limit used
used_percentage = ((limit - remaining) / limit) * 100

if remaining < 50:
    print(f'Rate limit critical: {remaining}/{limit} requests remaining ({used_percentage:.1f}% used)')
elif remaining < 100:
    print(f'Rate limit warning: {remaining}/{limit} requests remaining ({used_percentage:.1f}% used)')

# Calculate time until reset
seconds_until_reset = reset_time - int(time.time())
if seconds_until_reset > 0:
    reset_datetime = datetime.fromtimestamp(reset_time)
    print(f'Rate limit resets in {seconds_until_reset} seconds at {reset_datetime}')
```

```go Go theme={null}
package main

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

func checkRateLimit(response *http.Response) {
    remainingStr := response.Header.Get("X-RateLimit-Remaining")
    resetStr := response.Header.Get("X-RateLimit-Reset")
    limitStr := response.Header.Get("X-RateLimit-Limit")
    
    remaining, _ := strconv.Atoi(remainingStr)
    resetTime, _ := strconv.ParseInt(resetStr, 10, 64)
    limit, _ := strconv.Atoi(limitStr)
    
    // Calculate percentage of rate limit used
    usedPercentage := float64(limit-remaining) / float64(limit) * 100
    
    if remaining < 50 {
        fmt.Printf("Rate limit critical: %d/%d requests remaining (%.1f%% used)\n", 
                  remaining, limit, usedPercentage)
    } else if remaining < 100 {
        fmt.Printf("Rate limit warning: %d/%d requests remaining (%.1f%% used)\n", 
                  remaining, limit, usedPercentage)
    }
    
    // Calculate time until reset
    now := time.Now().Unix()
    secondsUntilReset := resetTime - now
    if secondsUntilReset > 0 {
        fmt.Printf("Rate limit resets in %d seconds\n", secondsUntilReset)
    }
}

func main() {
    client := &http.Client{}
    req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers", nil)
    req.Header.Add("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
    
    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Error making request: %v\n", err)
        return
    }
    defer resp.Body.Close()
    
    checkRateLimit(resp)
}
```

```rust Rust theme={null}
use reqwest::header::{HeaderMap, HeaderValue};
use std::time::{SystemTime, UNIX_EPOCH};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    
    let mut headers = HeaderMap::new();
    headers.insert("X-API-Key", HeaderValue::from_static("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="));
    
    let response = client
        .get("https://api.platform.dakota.xyz/customers")
        .headers(headers)
        .send()
        .await?;
    
    // Extract rate limit headers
    let remaining: i32 = response
        .headers()
        .get("X-RateLimit-Remaining")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    
    let reset_time: u64 = response
        .headers()
        .get("X-RateLimit-Reset")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    
    let limit: i32 = response
        .headers()
        .get("X-RateLimit-Limit")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse().ok())
        .unwrap_or(1000);
    
    // Calculate percentage of rate limit used
    let used_percentage = ((limit - remaining) as f64 / limit as f64) * 100.0;
    
    if remaining < 50 {
        println!("Rate limit critical: {}/{} requests remaining ({:.1}% used)", 
                remaining, limit, used_percentage);
    } else if remaining < 100 {
        println!("Rate limit warning: {}/{} requests remaining ({:.1}% used)", 
                remaining, limit, used_percentage);
    }
    
    // Calculate time until reset
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)?
        .as_secs();
    
    if reset_time > now {
        let seconds_until_reset = reset_time - now;
        println!("Rate limit resets in {} seconds", seconds_until_reset);
    }
    
    Ok(())
}
```

```java 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 DakotaRateLimitMonitor {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.platform.dakota.xyz/customers"))
            .header("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
            .GET()
            .build();
            
        HttpResponse<String> response = client.send(request, 
            HttpResponse.BodyHandlers.ofString());
        
        checkRateLimit(response);
    }
    
    public static void checkRateLimit(HttpResponse<String> response) {
        String remainingStr = response.headers().firstValue("X-RateLimit-Remaining").orElse("0");
        String resetStr = response.headers().firstValue("X-RateLimit-Reset").orElse("0");
        String limitStr = response.headers().firstValue("X-RateLimit-Limit").orElse("1000");
        
        int remaining = Integer.parseInt(remainingStr);
        long resetTime = Long.parseLong(resetStr);
        int limit = Integer.parseInt(limitStr);
        
        // Calculate percentage of rate limit used
        double usedPercentage = ((double)(limit - remaining) / limit) * 100;
        
        if (remaining < 50) {
            System.out.printf("Rate limit critical: %d/%d requests remaining (%.1f%% used)%n", 
                            remaining, limit, usedPercentage);
        } else if (remaining < 100) {
            System.out.printf("Rate limit warning: %d/%d requests remaining (%.1f%% used)%n", 
                            remaining, limit, usedPercentage);
        }
        
        // Calculate time until reset
        long now = Instant.now().getEpochSecond();
        long secondsUntilReset = resetTime - now;
        if (secondsUntilReset > 0) {
            System.out.printf("Rate limit resets in %d seconds%n", secondsUntilReset);
        }
    }
}
```

### 2. Implement Exponential Backoff

When you receive a 429 response, implement exponential backoff with jitter:

```javascript JavaScript theme={null}
class DakotaApiClient {
  constructor(apiKey, baseUrl = 'https://api.platform.dakota.xyz') {
    this.apiKey = apiKey;
    this.baseUrl = baseUrl;
    this.maxRetries = 5;
    this.baseDelay = 1000; // 1 second
  }

  async makeRequest(endpoint, options = {}) {
    const url = `${this.baseUrl}${endpoint}`;
    const requestOptions = {
      ...options,
      headers: {
        'X-API-Key': this.apiKey,
        'Content-Type': 'application/json',
        ...options.headers
      }
    };

    // Add idempotency key for POST requests
    if (options.method === 'POST') {
      requestOptions.headers['X-Idempotency-Key'] = crypto.randomUUID();
    }

    return this.makeRequestWithBackoff(url, requestOptions);
  }

  async makeRequestWithBackoff(url, options, attempt = 0) {
    try {
      const response = await fetch(url, options);
      
      // Handle rate limiting
      if (response.status === 429) {
        if (attempt >= this.maxRetries) {
          throw new Error(`Rate limit exceeded after ${this.maxRetries} retries`);
        }
        
        return this.retryWithBackoff(url, options, attempt);
      }
      
      // Handle other 5xx errors with retry
      if (response.status >= 500 && attempt < this.maxRetries) {
        return this.retryWithBackoff(url, options, attempt);
      }
      
      return response;
    } catch (error) {
      if (error.name === 'TypeError' && attempt < this.maxRetries) {
        // Network error, retry
        return this.retryWithBackoff(url, options, attempt);
      }
      throw error;
    }
  }

  async retryWithBackoff(url, options, attempt) {
    const retryAfter = 1; // Default to 1 second if no Retry-After header
    const exponentialDelay = this.baseDelay * Math.pow(2, attempt);
    const jitter = Math.random() * 1000;
    const totalDelay = Math.min(exponentialDelay + jitter, 60000); // Cap at 60s
    
    console.log(`Rate limited. Retrying in ${totalDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
    
    await new Promise(resolve => setTimeout(resolve, totalDelay));
    return this.makeRequestWithBackoff(url, options, attempt + 1);
  }
}

// Usage example
const dakota = new DakotaApiClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=');

try {
  const customers = await dakota.makeRequest('/customers');
  console.log('Customers retrieved:', customers.data);
} catch (error) {
  console.error('Failed after retries:', error.message);
}
```

```python Python theme={null}
import time
import random
import requests
import uuid
from typing import Optional, Dict, Any

class DakotaApiClient:
    def __init__(self, api_key: str, base_url: str = 'https://api.platform.dakota.xyz'):
        self.api_key = api_key
        self.base_url = base_url
        self.max_retries = 5
        self.base_delay = 1  # 1 second
        self.session = requests.Session()
        self.session.headers.update({
            'X-API-Key': api_key,
            'Content-Type': 'application/json'
        })
    
    def make_request(self, endpoint: str, method: str = 'GET', 
                    data: Optional[Dict[Any, Any]] = None, 
                    headers: Optional[Dict[str, str]] = None) -> requests.Response:
        url = f'{self.base_url}{endpoint}'
        request_headers = headers or {}
        
        # Add idempotency key for POST requests
        if method.upper() == 'POST':
            request_headers['X-Idempotency-Key'] = str(uuid.uuid4())
        
        return self._make_request_with_backoff(url, method, data, request_headers)
    
    def _make_request_with_backoff(self, url: str, method: str, 
                                  data: Optional[Dict[Any, Any]], 
                                  headers: Dict[str, str], 
                                  attempt: int = 0) -> requests.Response:
        try:
            response = self.session.request(
                method, url, json=data, headers=headers, timeout=30
            )
            
            # Handle rate limiting
            if response.status_code == 429:
                if attempt >= self.max_retries:
                    raise Exception(f'Rate limit exceeded after {self.max_retries} retries')
                
                return self._retry_with_backoff(url, method, data, headers, attempt, response)
            
            # Handle server errors with retry
            if response.status_code >= 500 and attempt < self.max_retries:
                return self._retry_with_backoff(url, method, data, headers, attempt, response)
            
            response.raise_for_status()
            return response
            
        except requests.exceptions.RequestException as e:
            if attempt < self.max_retries:
                return self._retry_with_backoff(url, method, data, headers, attempt)
            raise e
    
    def _retry_with_backoff(self, url: str, method: str, 
                           data: Optional[Dict[Any, Any]], 
                           headers: Dict[str, str], attempt: int, 
                           response: Optional[requests.Response] = None) -> requests.Response:
        retry_after = 1  # Default to 1 second
        if response and 'Retry-After' in response.headers:
            retry_after = int(response.headers['Retry-After'])
        
        exponential_delay = self.base_delay * (2 ** attempt)
        jitter = random.uniform(0, 1)
        total_delay = min(exponential_delay + jitter, 60)  # Cap at 60 seconds
        
        print(f'Rate limited. Retrying in {total_delay:.2f}s (attempt {attempt + 1}/{self.max_retries})')
        
        time.sleep(total_delay)
        return self._make_request_with_backoff(url, method, data, headers, attempt + 1)

# Usage example
dakota = DakotaApiClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=')

try:
    response = dakota.make_request('/customers')
    customers = response.json()
    print(f'Customers retrieved: {customers["data"]}')
except Exception as e:
    print(f'Failed after retries: {e}')
```

```go Go theme={null}
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "math"
    "math/rand"
    "net/http"
    "strconv"
    "time"
    "github.com/google/uuid"
)

type DakotaApiClient struct {
    ApiKey     string
    BaseURL    string
    MaxRetries int
    BaseDelay  time.Duration
    Client     *http.Client
}

func NewDakotaApiClient(apiKey string) *DakotaApiClient {
    return &DakotaApiClient{
        ApiKey:     apiKey,
        BaseURL:    "https://api.platform.dakota.xyz",
        MaxRetries: 5,
        BaseDelay:  time.Second,
        Client:     &http.Client{Timeout: 30 * time.Second},
    }
}

func (c *DakotaApiClient) MakeRequest(endpoint, method string, data interface{}) (*http.Response, error) {
    url := c.BaseURL + endpoint
    
    var body io.Reader
    if data != nil {
        jsonData, err := json.Marshal(data)
        if err != nil {
            return nil, err
        }
        body = bytes.NewBuffer(jsonData)
    }
    
    req, err := http.NewRequest(method, url, body)
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("X-API-Key", c.ApiKey)
    req.Header.Set("Content-Type", "application/json")
    
    if method == "POST" {
        req.Header.Set("X-Idempotency-Key", uuid.New().String())
    }
    
    return c.makeRequestWithBackoff(req, 0)
}

func (c *DakotaApiClient) makeRequestWithBackoff(req *http.Request, attempt int) (*http.Response, error) {
    resp, err := c.Client.Do(req)
    if err != nil {
        if attempt < c.MaxRetries {
            return c.retryWithBackoff(req, attempt, nil)
        }
        return nil, err
    }
    
    // Handle rate limiting
    if resp.StatusCode == 429 {
        if attempt >= c.MaxRetries {
            return nil, fmt.Errorf("rate limit exceeded after %d retries", c.MaxRetries)
        }
        return c.retryWithBackoff(req, attempt, resp)
    }
    
    // Handle server errors with retry
    if resp.StatusCode >= 500 && attempt < c.MaxRetries {
        return c.retryWithBackoff(req, attempt, resp)
    }
    
    return resp, nil
}

func (c *DakotaApiClient) retryWithBackoff(req *http.Request, attempt int, resp *http.Response) (*http.Response, error) {
    retryAfter := 1 * time.Second
    if resp != nil {
        if retryAfterHeader := resp.Header.Get("Retry-After"); retryAfterHeader != "" {
            if seconds, err := strconv.Atoi(retryAfterHeader); err == nil {
                retryAfter = time.Duration(seconds) * time.Second
            }
        }
    }
    
    exponentialDelay := c.BaseDelay * time.Duration(math.Pow(2, float64(attempt)))
    jitter := time.Duration(rand.Float64() * float64(time.Second))
    totalDelay := exponentialDelay + jitter
    
    if totalDelay > 60*time.Second {
        totalDelay = 60 * time.Second
    }
    
    fmt.Printf("Rate limited. Retrying in %v (attempt %d/%d)\n", totalDelay, attempt+1, c.MaxRetries)
    
    time.Sleep(totalDelay)
    return c.makeRequestWithBackoff(req, attempt+1)
}

func main() {
    dakota := NewDakotaApiClient("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
    
    resp, err := dakota.MakeRequest("/customers", "GET", nil)
    if err != nil {
        fmt.Printf("Failed after retries: %v\n", err)
        return
    }
    defer resp.Body.Close()
    
    fmt.Println("Request succeeded!")
}
```

```rust Rust theme={null}
use reqwest::header::{HeaderMap, HeaderValue};
use serde_json::Value;
use std::time::Duration;
use tokio::time::sleep;
use uuid::Uuid;
use rand::Rng;

pub struct DakotaApiClient {
    api_key: String,
    base_url: String,
    max_retries: u32,
    base_delay: Duration,
    client: reqwest::Client,
}

impl DakotaApiClient {
    pub fn new(api_key: String) -> Self {
        Self {
            api_key,
            base_url: "https://api.platform.dakota.xyz".to_string(),
            max_retries: 5,
            base_delay: Duration::from_secs(1),
            client: reqwest::Client::builder()
                .timeout(Duration::from_secs(30))
                .build()
                .unwrap(),
        }
    }
    
    pub async fn make_request(
        &self,
        endpoint: &str,
        method: reqwest::Method,
        data: Option<&Value>,
    ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
        let url = format!("{}{}", self.base_url, endpoint);
        
        let mut headers = HeaderMap::new();
        headers.insert("X-API-Key", HeaderValue::from_str(&self.api_key)?);
        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
        
        if method == reqwest::Method::POST {
            headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?);
        }
        
        let mut request_builder = self.client.request(method, &url).headers(headers);
        
        if let Some(json_data) = data {
            request_builder = request_builder.json(json_data);
        }
        
        self.make_request_with_backoff(request_builder, 0).await
    }
    
    async fn make_request_with_backoff(
        &self,
        request_builder: reqwest::RequestBuilder,
        attempt: u32,
    ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
        let request = request_builder.try_clone()
            .ok_or("Failed to clone request")?;
        
        let response = self.client.execute(request.build()?).await;
        
        match response {
            Ok(resp) => {
                if resp.status() == 429 {
                    if attempt >= self.max_retries {
                        return Err(format!("Rate limit exceeded after {} retries", self.max_retries).into());
                    }
                    return self.retry_with_backoff(request_builder, attempt, Some(&resp)).await;
                }
                
                if resp.status().as_u16() >= 500 && attempt < self.max_retries {
                    return self.retry_with_backoff(request_builder, attempt, Some(&resp)).await;
                }
                
                Ok(resp)
            }
            Err(e) => {
                if attempt < self.max_retries {
                    return self.retry_with_backoff(request_builder, attempt, None).await;
                }
                Err(e.into())
            }
        }
    }
    
    async fn retry_with_backoff(
        &self,
        request_builder: reqwest::RequestBuilder,
        attempt: u32,
        response: Option<&reqwest::Response>,
    ) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
        let mut retry_after = Duration::from_secs(1);
        
        if let Some(resp) = response {
            if let Some(retry_header) = resp.headers().get("Retry-After") {
                if let Ok(seconds_str) = retry_header.to_str() {
                    if let Ok(seconds) = seconds_str.parse::<u64>() {
                        retry_after = Duration::from_secs(seconds);
                    }
                }
            }
        }
        
        let exponential_delay = self.base_delay * 2_u32.pow(attempt);
        let jitter = Duration::from_millis(rand::thread_rng().gen_range(0..1000));
        let total_delay = std::cmp::min(exponential_delay + jitter, Duration::from_secs(60));
        
        println!("Rate limited. Retrying in {:?} (attempt {}/{})", total_delay, attempt + 1, self.max_retries);
        
        sleep(total_delay).await;
        self.make_request_with_backoff(request_builder, attempt + 1).await
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dakota = DakotaApiClient::new("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=".to_string());
    
    match dakota.make_request("/customers", reqwest::Method::GET, None).await {
        Ok(_) => println!("Request succeeded!"),
        Err(e) => println!("Failed after retries: {}", e),
    }
    
    Ok(())
}
```

```java Java theme={null}
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;

public class DakotaApiClient {
    private final String apiKey;
    private final String baseUrl;
    private final int maxRetries;
    private final Duration baseDelay;
    private final HttpClient client;
    private final Random random;
    
    public DakotaApiClient(String apiKey) {
        this.apiKey = apiKey;
        this.baseUrl = "https://api.platform.dakota.xyz";
        this.maxRetries = 5;
        this.baseDelay = Duration.ofSeconds(1);
        this.client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
        this.random = new Random();
    }
    
    public HttpResponse<String> makeRequest(String endpoint, String method, String jsonData) 
            throws IOException, InterruptedException {
        String url = baseUrl + endpoint;
        
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("X-API-Key", apiKey)
            .header("Content-Type", "application/json");
        
        if ("POST".equalsIgnoreCase(method)) {
            requestBuilder.header("X-Idempotency-Key", UUID.randomUUID().toString());
            if (jsonData != null) {
                requestBuilder.POST(HttpRequest.BodyPublishers.ofString(jsonData));
            } else {
                requestBuilder.POST(HttpRequest.BodyPublishers.noBody());
            }
        } else {
            requestBuilder.GET();
        }
        
        return makeRequestWithBackoff(requestBuilder.build(), 0);
    }
    
    private HttpResponse<String> makeRequestWithBackoff(HttpRequest request, int attempt) 
            throws IOException, InterruptedException {
        try {
            HttpResponse<String> response = client.send(request, 
                HttpResponse.BodyHandlers.ofString());
            
            // Handle rate limiting
            if (response.statusCode() == 429) {
                if (attempt >= maxRetries) {
                    throw new RuntimeException("Rate limit exceeded after " + maxRetries + " retries");
                }
                return retryWithBackoff(request, attempt, response);
            }
            
            // Handle server errors with retry
            if (response.statusCode() >= 500 && attempt < maxRetries) {
                return retryWithBackoff(request, attempt, response);
            }
            
            return response;
        } catch (IOException e) {
            if (attempt < maxRetries) {
                return retryWithBackoff(request, attempt, null);
            }
            throw e;
        }
    }
    
    private HttpResponse<String> retryWithBackoff(HttpRequest request, int attempt, 
            HttpResponse<String> response) throws IOException, InterruptedException {
        Duration retryAfter = Duration.ofSeconds(1);
        
        if (response != null) {
            Optional<String> retryAfterHeader = response.headers().firstValue("Retry-After");
            if (retryAfterHeader.isPresent()) {
                try {
                    int seconds = Integer.parseInt(retryAfterHeader.get());
                    retryAfter = Duration.ofSeconds(seconds);
                } catch (NumberFormatException ignored) {
                    // Use default retry after
                }
            }
        }
        
        long exponentialDelayMs = baseDelay.toMillis() * (long) Math.pow(2, attempt);
        long jitterMs = random.nextInt(1000);
        long totalDelayMs = Math.min(exponentialDelayMs + jitterMs, 60000); // Cap at 60s
        
        System.out.printf("Rate limited. Retrying in %dms (attempt %d/%d)%n", 
                         totalDelayMs, attempt + 1, maxRetries);
        
        Thread.sleep(totalDelayMs);
        return makeRequestWithBackoff(request, attempt + 1);
    }
    
    public static void main(String[] args) {
        DakotaApiClient dakota = new DakotaApiClient("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=");
        
        try {
            HttpResponse<String> response = dakota.makeRequest("/customers", "GET", null);
            System.out.println("Request succeeded!");
        } catch (Exception e) {
            System.out.println("Failed after retries: " + e.getMessage());
        }
    }
}
```

### 3. Distribute Requests Over Time

Instead of making all requests at once, distribute them evenly:

```javascript JavaScript theme={null}
class RateLimitedClient {
  constructor(apiKey, requestsPerMinute = 900) {
    this.apiKey = apiKey;
    this.interval = 60000 / requestsPerMinute; // ms between requests
    this.lastRequest = 0;
  }
  
  async makeRequest(url, options = {}) {
    const now = Date.now();
    const timeToWait = this.interval - (now - this.lastRequest);
    
    if (timeToWait > 0) {
      await new Promise(resolve => setTimeout(resolve, timeToWait));
    }
    
    this.lastRequest = Date.now();
    
    return fetch(url, {
      ...options,
      headers: {
        'X-API-Key': this.apiKey,
        ...options.headers
      }
    });
  }
}
```

## Best Practices

### 1. Stay Under the Limit

* Target 90% of your rate limit (900 requests/minute) to leave buffer room
* Monitor your usage patterns and adjust accordingly

### 2. Batch Operations

* Use bulk endpoints when available
* Group related operations together

### 3. Cache Results

* Cache API responses when appropriate to reduce API calls
* Use ETags or last-modified headers for efficient caching

### 4. Implement Circuit Breaker

* Stop making requests temporarily after multiple rate limit errors
* Gradually resume requests after cooling down

```javascript JavaScript theme={null}
class DakotaCircuitBreaker {
  constructor(options = {}) {
    this.failures = 0;
    this.successCount = 0;
    this.threshold = options.threshold || 5; // Failures before opening
    this.timeout = options.timeout || 60000; // 1 minute
    this.resetTimeout = options.resetTimeout || 30000; // 30 seconds
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.nextAttempt = Date.now();
    this.lastFailureTime = null;
  }
  
  async call(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error(`Circuit breaker is OPEN. Next attempt in ${Math.ceil((this.nextAttempt - Date.now()) / 1000)}s`);
      }
      this.state = 'HALF_OPEN';
      console.log('Circuit breaker is now HALF_OPEN, attempting request...');
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure(error);
      throw error;
    }
  }
  
  onSuccess() {
    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      
      // After 3 successful requests in HALF_OPEN, close the circuit
      if (this.successCount >= 3) {
        console.log('Circuit breaker is now CLOSED after successful requests');
        this.failures = 0;
        this.successCount = 0;
        this.state = 'CLOSED';
      }
    } else {
      this.failures = 0;
      this.state = 'CLOSED';
    }
  }
  
  onFailure(error) {
    this.failures++;
    this.lastFailureTime = Date.now();
    
    // Only count rate limit and server errors towards circuit breaking
    const isCircuitBreakerError = error.message.includes('429') || 
                                 error.message.includes('Rate limit') ||
                                 error.message.includes('500') ||
                                 error.message.includes('502') ||
                                 error.message.includes('503');
    
    if (isCircuitBreakerError && this.failures >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
      this.successCount = 0;
      console.log(`Circuit breaker is now OPEN after ${this.failures} failures. Will retry at ${new Date(this.nextAttempt)}`);
    }
  }
  
  getStatus() {
    return {
      state: this.state,
      failures: this.failures,
      nextAttempt: this.state === 'OPEN' ? new Date(this.nextAttempt) : null,
      lastFailure: this.lastFailureTime ? new Date(this.lastFailureTime) : null
    };
  }
}

// Enhanced Dakota client with circuit breaker
class EnhancedDakotaClient extends DakotaApiClient {
  constructor(apiKey, baseUrl) {
    super(apiKey, baseUrl);
    this.circuitBreaker = new DakotaCircuitBreaker({
      threshold: 5,
      timeout: 60000, // 1 minute
      resetTimeout: 30000 // 30 seconds
    });
  }

  async makeRequest(endpoint, options = {}) {
    return this.circuitBreaker.call(async () => {
      return super.makeRequest(endpoint, options);
    });
  }

  getCircuitBreakerStatus() {
    return this.circuitBreaker.getStatus();
  }
}

// Usage example with circuit breaker
const dakota = new EnhancedDakotaClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=');

// Make requests with automatic circuit breaking
for (let i = 0; i < 20; i++) {
  try {
    const response = await dakota.makeRequest('/customers');
    console.log(`Request ${i + 1} succeeded`);
  } catch (error) {
    console.error(`Request ${i + 1} failed:`, error.message);
    
    // Check circuit breaker status
    const status = dakota.getCircuitBreakerStatus();
    if (status.state === 'OPEN') {
      console.log('Circuit breaker is open, waiting before next attempt...');
      await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
    }
  }
}
```

### 5. Request Queuing and Throttling

For high-volume applications, implement request queuing to stay within rate limits:

```javascript JavaScript theme={null}
class DakotaRequestQueue {
  constructor(apiClient, requestsPerMinute = 900) {
    this.apiClient = apiClient;
    this.queue = [];
    this.processing = false;
    this.interval = 60000 / requestsPerMinute; // ms between requests
    this.lastRequest = 0;
  }

  async enqueue(endpoint, options = {}) {
    return new Promise((resolve, reject) => {
      this.queue.push({ endpoint, options, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.queue.length === 0) {
      return;
    }

    this.processing = true;

    while (this.queue.length > 0) {
      const { endpoint, options, resolve, reject } = this.queue.shift();
      
      try {
        // Ensure minimum interval between requests
        const now = Date.now();
        const timeToWait = this.interval - (now - this.lastRequest);
        
        if (timeToWait > 0) {
          await new Promise(r => setTimeout(r, timeToWait));
        }
        
        this.lastRequest = Date.now();
        const response = await this.apiClient.makeRequest(endpoint, options);
        resolve(response);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }

  getQueueStatus() {
    return {
      queueLength: this.queue.length,
      processing: this.processing,
      estimatedWaitTime: this.queue.length * this.interval
    };
  }
}

// Usage example with request queue
const dakota = new DakotaApiClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=');
const requestQueue = new DakotaRequestQueue(dakota, 900); // 900 requests per minute

// Queue multiple requests
const requests = [];
for (let i = 0; i < 100; i++) {
  requests.push(
    requestQueue.enqueue('/customers', { 
      method: 'POST', 
      body: JSON.stringify({ 
        customer_type: 'individual',
        name: `Customer ${i}` 
      }) 
    })
  );
}

// Process all requests with automatic rate limiting
const results = await Promise.allSettled(requests);
console.log(`Processed ${results.filter(r => r.status === 'fulfilled').length} requests successfully`);
```

## Need Higher Limits?

If you need higher rate limits for your use case, contact our support team with:

* Your current usage patterns and peak request volumes
* Expected request volume and growth projections
* Business justification for higher limits
* Timeline for when you need the increase
* Description of your rate limiting and retry strategies

We'll work with you to find a solution that meets your needs while maintaining API performance for all users.

### Enterprise Rate Limits

For enterprise customers, we offer:

* **Higher base limits**: Up to 5,000 requests per minute
* **Burst allowances**: Short-term higher limits for batch operations
* **Dedicated rate limit monitoring**: Real-time alerts and usage analytics
* **Custom retry strategies**: Optimized backoff algorithms for your use case
