Patterns d'intégration
Bonnes pratiques pour intégrer l'API The Trade Hub en production - polling, batch, gestion d'erreurs et rate limiting.
Ce guide présente les patterns recommandés pour intégrer l'API The Trade Hub dans vos applications de production. Vous y trouverez des exemples concrets en TypeScript et Python.
Architecture de l'API
L'API The Trade Hub utilise un modèle asynchrone par jobs pour les opérations de classement. Ce choix architectural permet de traiter des requêtes complexes sans bloquer le client.
Client ──POST /v1/classify/jobs──> API ──> Job cree
Client <── id ──────────────────────────────────┘
Client ──GET /v1/classify/jobs/:id──> API ──> Statut du job
Client <── status + result ────────────────────────────────┘Pattern de polling
Le polling est le pattern principal pour récupérer les résultats de classement.
Implémentation recommandée
interface Ranking {
rank: number;
hs_code: string;
description: string;
confidence: number;
gri_justification: string;
validated: boolean;
}
interface ClassifyResult {
classification: { rankings: Ranking[] };
text: string;
session_id: string;
}
interface JobResponse {
id: string;
status: 'pending' | '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. Créer le job
const { id: job_id } = await this.request<{ id: string }>('/classify/jobs', {
method: 'POST',
body: JSON.stringify({
content,
origin_country: options?.originCountry,
}),
});
// 2. Polling avec backoff exponentiel
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/jobs/${jobId}`);
if (job.status === 'completed' && job.result) {
return job.result;
}
if (job.status === 'failed') {
throw new Error(`Classification failed: ${job.error}`);
}
// Backoff exponentiel avec jitter
await new Promise((resolve) =>
setTimeout(resolve, delay + Math.random() * 200)
);
delay = Math.min(delay * 1.5, 5000); // Max 5 secondes entre les tentatives
}
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 Ranking:
rank: int
hs_code: str
description: str
confidence: float
gri_justification: str
validated: bool
@dataclass
class ClassifyResult:
rankings: list # list[Ranking]
text: str
session_id: 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. Créer le job
payload = {"content": content}
if origin_country:
payload["origin_country"] = origin_country
response = self.session.post(
f"{self.base_url}/classify/jobs",
json=payload,
)
response.raise_for_status()
job_id = response.json()["id"]
# 2. Polling avec backoff exponentiel
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/jobs/{job_id}"
)
response.raise_for_status()
data = response.json()
if data["status"] == "completed":
r = data["result"]
return ClassifyResult(
rankings=r["classification"]["rankings"],
text=r.get("text", ""),
session_id=r.get("session_id", ""),
)
if data["status"] == "failed":
raise ApiError(500, f"Classification failed: {data.get('error')}")
# Backoff exponentiel avec 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")Traitement par lots (batch)
Pour classer un grand nombre de produits, utilisez l'endpoint batch qui optimise le traitement côté serveur.
interface BatchItem {
id: string; // Votre identifiant interne
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 à 1
}
async function classifyBatch(
client: TradeHubClient,
items: BatchItem[]
): Promise<BatchResponse> {
// 1. Soumettre le batch
const { id: batch_id } = await client.request<{ id: string }>(
'/classify/batch',
{
method: 'POST',
body: JSON.stringify({ items }),
}
);
// 2. Polling du batch (intervalles plus longs)
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');
}
// Utilisation
const items: BatchItem[] = [
{ id: 'SKU-001', content: 'Câble USB-C vers Lightning' },
{ id: 'SKU-002', content: 'Coque de protection iPhone 15 en silicone' },
{ id: 'SKU-003', content: 'Chargeur sans fil Qi 15W' },
];
const results = await classifyBatch(client, items);Quand utiliser le batch
| Scénario | Pattern recommandé |
|---|---|
| 1 à 5 produits | Requêtes individuelles en parallèle |
| 6 à 100 produits | Endpoint batch |
| 100+ produits | Batch avec pagination (lots de 100) |
| Temps réel (1 produit) | Requête individuelle |
Gestion des erreurs
Codes d'erreur HTTP
| Code | Signification | Action recommandée |
|---|---|---|
| 400 | Requête invalide | Vérifier le format des données |
| 401 | Clé API invalide | Vérifier la clé API |
| 403 | Accès refusé | Vérifier les permissions de l'organisation |
| 404 | Ressource non trouvée | Vérifier l'identifiant du job |
| 429 | Rate limit dépassé | Attendre et réessayer (voir headers) |
| 500 | Erreur serveur | Réessayer avec backoff |
| 503 | Service indisponible | Réessayer après quelques secondes |
Logique de retry
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;
// Ne pas retrier les erreurs client (4xx sauf 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;
}
// Utilisation
const result = await withRetry(() => client.classify('Mon produit'));
const topRanking = result.classification.rankings[0];
console.log(`Code HS: ${topRanking.hs_code} (confiance: ${topRanking.confidence})`);Rate limiting
L'API applique des limites de requêtes pour garantir la qualité de service. Les limites sont communiquées via les headers de réponse.
Headers de rate limiting
| Header | Description |
|---|---|
X-RateLimit-Limit | Nombre maximum de requêtes par fenêtre (par bucket) |
X-RateLimit-Remaining | Requêtes restantes dans la fenêtre courante |
Retry-After | Secondes à attendre (uniquement sur 429) |
Buckets séparés
L'API utilise deux buckets indépendants par clé API :
| Bucket | Méthodes HTTP | Limite | Raison |
|---|---|---|---|
| Ecriture | POST, PUT, PATCH, DELETE | 10/min | Opérations LLM (chaque SSE reste ouvert ~90s) |
| Lecture | GET, HEAD, OPTIONS | 60/min | Polling de statut (toutes les 2s = 30/min typique) |
Un appel batch compte comme une seule requête d'écriture. La limite par clé peut être abaissée via le tableau de bord développeur.
Implémentation du throttling
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) {
// Fenêtre fixe de 1 minute - attendre le reset
await new Promise((r) => setTimeout(r, 60_000));
this.remaining = this.limit;
}
this.remaining--;
}
}Facturation
L'API utilise un modèle pay-per-use avec tarification graduée. Il n'y a pas de plans fixes. Voir la documentation complète pour les détails.
Webhook (bientôt disponible)
Le pattern webhook permettra de recevoir les résultats directement sur votre serveur sans avoir à faire du polling.
// Enregistrer un webhook
await client.request('/webhooks', {
method: 'POST',
body: JSON.stringify({
url: 'https://api.votredomaine.com/webhooks/tradehub',
events: ['classification.completed', 'classification.failed'],
secret: 'whsec_votre_secret_de_verification',
}),
});
// Votre endpoint de réception
app.post('/webhooks/tradehub', (req, res) => {
// Vérifier la 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');
});Bonnes pratiques en production
- Toujours implémenter le retry avec backoff exponentiel pour les erreurs 5xx et 429
- Respecter les headers de rate limiting pour éviter les blocages
- Utiliser le batch pour plus de 5 classements simultanes
- Stocker les résultats en cache local pour éviter de reclasser les mêmes produits
- Journaliser les erreurs pour détecter les patterns de défaillance
- Utiliser des timeouts côté client (recommandé : 30 secondes par requête)
- Valider les entrées avant d'envoyer les requêtes (description non vide, code pays valide)
- Gérer les résultats partiels dans le batch (certains items peuvent échouer)
Dernière mise à jour
Flux d'exportation - Intégration API
Comment utiliser l'API The Trade Hub dans un flux d'exportation - classement, contrôle export et bonnes pratiques.
Nouveau Code des douanes 2026 - Guide complet de la recodification (ordonnance 2026-265)
Guide de référence sur le nouveau Code des douanes français recodifié par l'ordonnance n. 2026-265 du 8 avril 2026. Structure en 7 livres, 890 articles, table de concordance complète avec l'ancien code (1 154 correspondances vérifiées), changements substantifs (droit à l'erreur, procédure contradictoire, droit de communication élargi), impacts par métier. En vigueur le 1er mai 2026.