Skip to main content
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 TypeRate LimitWindow
API Key (x-api-key)60 requestsper minute
JWT (Dashboard users)600 requestsper minute
Unauthenticated10 requestsper minute
Application Token100 requestsper hour

Rate Limit Headers

Every API response includes rate limit information in the headers:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in window
X-RateLimit-RemainRequests remaining in current window
X-RateLimit-ResetSeconds until rate limit resets

Example Response Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remain: 47
X-RateLimit-Reset: 45
Content-Type: application/json

429 Rate Limit Exceeded

When you exceed the rate limit, you’ll receive a 429 Too Many Requests response:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remain: 0
X-RateLimit-Reset: 30
Content-Type: application/json

{
  "code": "rate_limit_exceeded",
  "message": "Too many requests. Please retry after the rate limit window resets."
}
The X-RateLimit-Reset 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
const response = await fetch('https://api.platform.dakota.xyz/customers', {
  headers: {
    'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='
  }
});

const remaining = parseInt(response.headers.get('X-RateLimit-Remain'));
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
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-Remain', 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
package main

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

func checkRateLimit(response *http.Response) {
    remainingStr := response.Header.Get("X-RateLimit-Remain")
    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
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-Remain")
        .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
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-Remain").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
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
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
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
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
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
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
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
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