Ohio Sales Tax Calculator

Rate Limits & Quotas

To keep the service available for all organizations, the Ohio Sales Tax API enforces two tiers of rate limiting:

  1. Per-Second Burst Limit: Sliding 1-second window preventing concurrent request spikes.
  2. Monthly Request Quota: Monthly volume allocated to your organization tier.

Plan Limits

Plan TierSliding Window (req/sec)Monthly Request Quota
Developer / Beta10 req/s1,000 / month
Ohio Sales Tax Pro50 req/s50,000 / month
Enterprise / POS250+ req/sUnlimited

Response Headers

Every HTTP response from /api/v1/rates includes standard RFC rate limit headers:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests permitted within the 1-second window.10
X-RateLimit-RemainingRemaining requests available in the current window.7
X-RateLimit-ResetUnix timestamp (in seconds) when the current window refreshes.1791036000
Retry-AfterReturned on 429 status codes. Number of seconds to pause before retrying.1

Rate Limit Exceeded (429 Too Many Requests)

When an application exceeds the sliding window or monthly quota, the API responds with:

HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 1
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791036001
{
  "type": "https://ohiosalestaxcalculator.com/errors/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit of 10 requests per second exceeded. Please back off and retry.",
  "request_id": "req_rl_49b1a0"
}

Production Retry Pattern (Exponential Backoff with Jitter)

Always implement exponential backoff with full jitter when consuming the API in high-concurrency production environments:

TypeScript / Node.js
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3): Promise<Response> {
  let attempt = 0;

  while (attempt < maxRetries) {
    const res = await fetch(url, options);

    if (res.status === 429) {
      attempt++;
      const retryAfterHeader = res.headers.get("Retry-After");
      const retryAfterSeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 1;

      // Add full jitter: random delay between 0 and exponential backoff
      const baseDelay = retryAfterSeconds * 1000 * Math.pow(2, attempt);
      const jitterDelay = Math.random() * baseDelay;

      console.warn(`[429] Rate limit reached. Retrying attempt ${attempt}/${maxRetries} after ${jitterDelay.toFixed(0)}ms`);
      await new Promise((resolve) => setTimeout(resolve, jitterDelay));
      continue;
    }

    return res;
  }

  throw new Error(`Max retries (${maxRetries}) exceeded for ${url}`);
}
Python
import time
import random
import requests

def fetch_with_retry(url, headers, params, max_retries=3):
    attempt = 0
    while attempt < max_retries:
        res = requests.get(url, headers=headers, params=params)
        if res.status_code == 429:
            attempt += 1
            retry_after = int(res.headers.get("Retry-After", 1))
            jitter_delay = random.uniform(0.5, 1.5) * (retry_after * (2 ** attempt))
            print(f"[429] Retrying after {jitter_delay:.2f}s...")
            time.sleep(jitter_delay)
            continue
        return res
    raise Exception(f"Max retries exceeded for {url}")

Caching Best Practices