Integration patterns
Best practices for integrating The Trade Hub API in production - polling, batch, error handling and rate limiting.
This guide presents the recommended patterns for integrating The Trade Hub API into your production applications. You will find concrete examples in TypeScript and Python.
API architecture
The Trade Hub API uses an asynchronous job model for classification operations. This architectural choice allows processing complex requests without blocking the client.
Client ──POST /classify──> API ──> Job created
Client <── job_id ────────────────────────┘
Client ──GET /classify/:id──> API ──> Job status
Client <── status + result ──────────────────────┘Polling pattern
Polling is the primary pattern for retrieving classification results.
Recommended implementation
interface ClassifyResult {
hs_code: string;
description: string;
confidence: number;
duty_rate: string;
measures: Array<{ type: string; rate: string; origin: string }>;
reasoning: string;
}
interface JobResponse {
job_id: string;
status: 'processing' | 'completed' | 'failed';
result?: ClassifyResult;
error?: string;
}
class TradeHubClient {
private baseUrl = 'https://api.thetradehub.eu/v1';
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error.detail ?? 'Unknown error');
}
return response.json() as Promise<T>;
}
async classify(
content: string,
options?: { originCountry?: string }
): Promise<ClassifyResult> {
// 1. Create the job
const { job_id } = await this.request<{ job_id: string }>('/classify', {
method: 'POST',
body: JSON.stringify({
content,
origin_country: options?.originCountry,
}),
});
// 2. Poll with exponential backoff
return this.pollResult(job_id);
}
private async pollResult(
jobId: string,
maxAttempts = 30,
initialDelay = 500
): Promise<ClassifyResult> {
let delay = initialDelay;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const job = await this.request<JobResponse>(`/classify/${jobId}`);
if (job.status === 'completed' && job.result) {
return job.result;
}
if (job.status === 'failed') {
throw new Error(`Classification failed: ${job.error}`);
}
// Exponential backoff with jitter
await new Promise((resolve) =>
setTimeout(resolve, delay + Math.random() * 200)
);
delay = Math.min(delay * 1.5, 5000); // Max 5 seconds between attempts
}
throw new Error(`Classification timed out after ${maxAttempts} attempts`);
}
}
class ApiError extends Error {
constructor(
public status: number,
message: string
) {
super(message);
this.name = 'ApiError';
}
}import time
import random
import requests
from dataclasses import dataclass
from typing import Optional
@dataclass
class ClassifyResult:
hs_code: str
description: str
confidence: float
duty_rate: str
measures: list
reasoning: str
class ApiError(Exception):
def __init__(self, status: int, message: str):
self.status = status
super().__init__(message)
class TradeHubClient:
def __init__(self, api_key: str):
self.base_url = "https://api.thetradehub.eu/v1"
self.session = requests.Session()
self.session.headers.update({
"X-API-Key": api_key,
"Content-Type": "application/json",
})
def classify(
self,
content: str,
origin_country: Optional[str] = None,
) -> ClassifyResult:
# 1. Create the job
payload = {"content": content}
if origin_country:
payload["origin_country"] = origin_country
response = self.session.post(
f"{self.base_url}/classify",
json=payload,
)
response.raise_for_status()
job_id = response.json()["job_id"]
# 2. Poll with exponential backoff
return self._poll_result(job_id)
def _poll_result(
self,
job_id: str,
max_attempts: int = 30,
initial_delay: float = 0.5,
) -> ClassifyResult:
delay = initial_delay
for _ in range(max_attempts):
response = self.session.get(
f"{self.base_url}/classify/{job_id}"
)
response.raise_for_status()
data = response.json()
if data["status"] == "completed":
r = data["result"]
return ClassifyResult(
hs_code=r["hs_code"],
description=r["description"],
confidence=r["confidence"],
duty_rate=r["duty_rate"],
measures=r["measures"],
reasoning=r["reasoning"],
)
if data["status"] == "failed":
raise ApiError(500, f"Classification failed: {data.get('error')}")
# Exponential backoff with jitter
time.sleep(delay + random.uniform(0, 0.2))
delay = min(delay * 1.5, 5.0)
raise TimeoutError(f"Classification timed out after {max_attempts} attempts")Batch processing
To classify a large number of products, use the batch endpoint which optimises processing on the server side.
interface BatchItem {
id: string; // Your internal identifier
content: string;
origin_country?: string;
}
interface BatchResponse {
batch_id: string;
total_items: number;
status: 'processing' | 'completed' | 'partial';
results: Array<{
id: string;
status: 'completed' | 'failed';
result?: ClassifyResult;
error?: string;
}>;
progress: number; // 0 to 1
}
async function classifyBatch(
client: TradeHubClient,
items: BatchItem[]
): Promise<BatchResponse> {
// 1. Submit the batch
const { batch_id } = await client.request<{ batch_id: string }>(
'/classify/batch',
{
method: 'POST',
body: JSON.stringify({ items }),
}
);
// 2. Poll the batch (longer intervals)
let delay = 2000;
const maxAttempts = 60;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const batch = await client.request<BatchResponse>(
`/classify/batch/${batch_id}`
);
if (batch.status === 'completed') {
return batch;
}
console.log(`Batch progress: ${Math.round(batch.progress * 100)}%`);
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.2, 10000);
}
throw new Error('Batch timed out');
}
// Usage
const items: BatchItem[] = [
{ id: 'SKU-001', content: 'USB-C to Lightning cable' },
{ id: 'SKU-002', content: 'iPhone 15 silicone protective case' },
{ id: 'SKU-003', content: 'Qi 15W wireless charger' },
];
const results = await classifyBatch(client, items);When to use batch
| Scenario | Recommended pattern |
|---|---|
| 1 to 5 products | Individual requests in parallel |
| 6 to 100 products | Batch endpoint |
| 100+ products | Batch with pagination (batches of 100) |
| Real-time (1 product) | Individual request |
Error handling
HTTP error codes
| Code | Meaning | Recommended action |
|---|---|---|
| 400 | Bad request | Check data format |
| 401 | Invalid API key | Check API key |
| 403 | Access denied | Check organisation permissions |
| 404 | Resource not found | Check job identifier |
| 429 | Rate limit exceeded | Wait and retry (see headers) |
| 500 | Server error | Retry with backoff |
| 503 | Service unavailable | Retry after a few seconds |
Retry logic
async function withRetry<T>(
fn: () => Promise<T>,
options = { maxRetries: 3, initialDelay: 1000 }
): Promise<T> {
let lastError: Error | undefined;
let delay = options.initialDelay;
for (let i = 0; i <= options.maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
// Do not retry client errors (4xx except 429)
if (error instanceof ApiError && error.status < 500 && error.status !== 429) {
throw error;
}
if (i < options.maxRetries) {
await new Promise((r) => setTimeout(r, delay + Math.random() * 500));
delay *= 2;
}
}
}
throw lastError;
}
// Usage
const result = await withRetry(() => client.classify('My product'));Rate limiting
The API applies request limits to ensure quality of service. Limits are communicated via response headers.
Rate limiting headers
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum number of requests per window (per bucket) |
X-RateLimit-Remaining | Remaining requests in the current window |
Retry-After | Seconds to wait (only on 429) |
Separate buckets
The API uses two independent buckets per API key:
| Bucket | HTTP methods | Limit | Rationale |
|---|---|---|---|
| Write | POST, PUT, PATCH, DELETE | 10/min | LLM operations (each SSE stays open ~90s) |
| Read | GET, HEAD, OPTIONS | 60/min | Status polling (every 2s = 30/min typical) |
A batch call counts as a single write request. The per-key limit can be lowered from the developer dashboard.
Throttling implementation
class RateLimiter {
private remaining: number;
constructor(private limit: number = 10) {
this.remaining = limit;
}
updateFromHeaders(headers: Headers): void {
const remaining = headers.get('X-RateLimit-Remaining');
if (remaining) this.remaining = parseInt(remaining, 10);
}
async waitIfNeeded(): Promise<void> {
if (this.remaining <= 0) {
// Fixed 1-minute window - wait for reset
await new Promise((r) => setTimeout(r, 60_000));
this.remaining = this.limit;
}
this.remaining--;
}
}Billing
The API uses a pay-per-use model with graduated pricing. There are no fixed plans. See the full documentation for details.
Webhook (coming soon)
The webhook pattern will allow you to receive results directly on your server without polling.
// Register a webhook
await client.request('/webhooks', {
method: 'POST',
body: JSON.stringify({
url: 'https://api.yourdomain.com/webhooks/tradehub',
events: ['classification.completed', 'classification.failed'],
secret: 'whsec_your_verification_secret',
}),
});
// Your receiving endpoint
app.post('/webhooks/tradehub', (req, res) => {
// Verify signature
const signature = req.headers['x-tradehub-signature'];
const isValid = verifySignature(req.body, signature, webhookSecret);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
if (event.type === 'classification.completed') {
processResult(event.data.job_id, event.data.result);
}
res.status(200).send('OK');
});Production best practices
- Always implement retry with exponential backoff for 5xx and 429 errors
- Respect rate limiting headers to avoid being blocked
- Use batch for more than 5 simultaneous classifications
- Cache results locally to avoid reclassifying the same products
- Log errors to detect failure patterns
- Use timeouts on the client side (recommended: 30 seconds per request)
- Validate inputs before sending requests (non-empty description, valid country code)
- Handle partial results in batch (some items may fail)
Last updated
Export flow - API integration
How to use The Trade Hub API in an export flow - classification, export control and best practices.
New French Customs Code 2026 - Complete recodification guide
Reference guide to the new French Customs Code (Code des douanes) recodified by ordonnance 2026-265. 7-book structure, article-by-article concordance with the old code, substantive changes, practical impacts by profession. In force May 1, 2026.