Rate Limiting
The Trade Hub API rate limiting - response headers, handling 429 errors and exponential backoff strategy.
The Trade Hub API enforces rate limiting to ensure service availability and fairness. The limit is configurable per API key.
Response headers
Every API response includes headers indicating your current usage:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum number of requests allowed per minute |
X-RateLimit-Remaining | Number of requests remaining in the current window |
Retry-After | Number of seconds to wait before retrying (only on 429 responses) |
Example headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
Content-Type: application/jsonDefault limits
The API uses separate buckets for write and read requests:
| Parameter | Value |
|---|---|
| Writes per minute (POST, PUT, PATCH, DELETE) | 10 (LLM operations, each SSE stays open ~90s) |
| Reads per minute (GET) | 60 (polling, status checks) |
| Max batch size | 1,000 items (10 MB max) |
| Max payload (classification) | 1 MB |
A batch call counts as a single write request. The per-key limit can be lowered (but not raised) from the developer dashboard.
429 response (Too Many Requests)
When the limit is reached, the API returns a 429 Too Many Requests error with a Retry-After header:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Content-Type: application/json
{
"detail": "rate limit exceeded"
}The Retry-After value indicates the number of seconds remaining before the window resets.
Handling rate limits
Recommended strategy: exponential backoff
When you receive a 429 response, wait for the duration specified by Retry-After before retrying. If the header is absent, use exponential backoff:
import httpx
import time
def request_with_retry(client: httpx.Client, method: str, url: str, max_retries: int = 5, **kwargs):
delay = 1
for attempt in range(max_retries):
response = client.request(method, url, **kwargs)
if response.status_code != 429:
return response
# Respect Retry-After if present
retry_after = response.headers.get("Retry-After")
if retry_after:
wait = int(retry_after)
else:
wait = delay
delay = min(delay * 2, 60)
print(f"Rate limited. Retrying in {wait}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
raise Exception("Maximum retries exceeded")
client = httpx.Client(
base_url="https://api.thetradehub.eu",
headers={"X-API-Key": "th_live_your_api_key"},
)
response = request_with_retry(client, "POST", "/v1/classify/jobs", json={
"content": "Wireless Bluetooth headphones"
})async function requestWithRetry(url, options, maxRetries = 5) {
let delay = 1000;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
// Respect Retry-After if present
const retryAfter = response.headers.get("Retry-After");
const wait = retryAfter ? parseInt(retryAfter) * 1000 : delay;
delay = Math.min(delay * 2, 60000);
console.log(`Rate limited. Retrying in ${wait / 1000}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise((r) => setTimeout(r, wait));
}
throw new Error("Maximum retries exceeded");
}
const response = await requestWithRetry(
"https://api.thetradehub.eu/v1/classify/jobs",
{
method: "POST",
headers: {
"X-API-Key": "th_live_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "Wireless Bluetooth headphones" }),
}
);Billing
The API uses a pay-per-use model with no fixed quotas. Each successful classification (HS or export control) is billed per unit with graduated pricing:
| Tier (units/month) | Price |
|---|---|
| 1 - 100 | EUR 0.75/unit |
| 101 - 1,000 | EUR 0.55/unit |
| 1,001 - 10,000 | EUR 0.40/unit |
| 10,001 - 50,000 | EUR 0.30/unit |
| 50,001+ | EUR 0.25/unit |
Monthly volumes for customs classification and export control are aggregated for tier calculation. Declarations (import, export, transit) use a flat rate of EUR 1.50/unit.
Only successfully completed classifications are billed. Failures and retries incur no cost.
Billing modes
| Mode | Description |
|---|---|
| Balance (prepaid) | Credits deducted in real time. Manual or automatic top-up. |
| Invoice (enterprise) | Monthly net-30 invoicing. Configurable spending limit. |
If the credit balance reaches zero (balance mode) or the spending limit is reached (invoice mode), the API returns a 402 Payment Required error.
Best practices
Monitor your headers
Check X-RateLimit-Remaining after each request. If the value approaches zero, proactively slow down your calls.
Use batch classification
To process multiple products, prefer the batch endpoint over sequential single calls. A batch call counts as a single request toward the rate limiter.
Spread your requests
If processing high volumes, distribute your requests evenly over time rather than sending them in bursts.
Cache results
Store classification results on the client side to avoid classifying the same product multiple times. Use the history endpoint to retrieve a past classification.
Never retry immediately
Retrying immediately after a 429 makes the situation worse. Always respect the Retry-After delay or apply exponential backoff.
Last updated