Integrationsmuster
Best Practices für die Integration der The Trade Hub API in der Produktion – Polling, Batch-Verarbeitung, Fehlerbehandlung und Ratenbegrenzung.
Dieser Leitfaden stellt die empfohlenen Muster für die Integration der The Trade Hub API in Ihre Produktionsanwendungen vor. Sie finden konkrete Beispiele in TypeScript und Python.
API-Architektur
Die The Trade Hub API verwendet ein asynchrones Job-Modell für Klassifizierungsoperationen. Diese architektonische Entscheidung ermöglicht die Verarbeitung komplexer Anfragen, ohne den Client zu blockieren.
Client ──POST /classify──> API ──> Job erstellt
Client <── job_id ──────────────────────────────┘
Client ──GET /classify/:id──> API ──> Job-Status
Client <── Status + Ergebnis ──────────────────────┘Polling-Muster
Polling ist das primäre Muster zum Abrufen von Klassifizierungsergebnissen.
Empfohlene Implementierung
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 ?? 'Unbekannter Fehler');
}
return response.json() as Promise<T>;
}
async classify(
content: string,
options?: { originCountry?: string }
): Promise<ClassifyResult> {
// 1. Job erstellen
const { job_id } = await this.request<{ job_id: string }>('/classify', {
method: 'POST',
body: JSON.stringify({
content,
origin_country: options?.originCountry,
}),
});
// 2. Polling mit exponentiellem 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(`Klassifizierung fehlgeschlagen: ${job.error}`);
}
// Exponentieller Backoff mit Jitter
await new Promise((resolve) =>
setTimeout(resolve, delay + Math.random() * 200)
);
delay = Math.min(delay * 1.5, 5000); // Maximal 5 Sekunden zwischen Versuchen
}
throw new Error(`Klassifizierung nach ${maxAttempts} Versuchen abgelaufen`);
}
}
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. Job erstellen
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. Polling mit exponentiellem 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"Klassifizierung fehlgeschlagen: {data.get('error')}")
# Exponentieller Backoff mit Jitter
time.sleep(delay + random.uniform(0, 0.2))
delay = min(delay * 1.5, 5.0)
raise TimeoutError(f"Klassifizierung nach {max_attempts} Versuchen abgelaufen")Batch-Verarbeitung
Um eine große Anzahl von Produkten zu klassifizieren, verwenden Sie den Batch-Endpunkt, der die Verarbeitung auf Serverseite optimiert.
interface BatchItem {
id: string; // Ihre interne Kennung
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 bis 1
}
async function classifyBatch(
client: TradeHubClient,
items: BatchItem[]
): Promise<BatchResponse> {
// 1. Batch übermitteln
const { batch_id } = await client.request<{ batch_id: string }>(
'/classify/batch',
{
method: 'POST',
body: JSON.stringify({ items }),
}
);
// 2. Batch abfragen (längere Intervalle)
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-Fortschritt: ${Math.round(batch.progress * 100)}%`);
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.2, 10000);
}
throw new Error('Batch-Zeitüberschreitung');
}
// Verwendung
const items: BatchItem[] = [
{ id: 'SKU-001', content: 'USB-C zu Lightning Kabel' },
{ id: 'SKU-002', content: 'iPhone 15 Silikon-Schutzhülle' },
{ id: 'SKU-003', content: 'Qi 15W kabelloses Ladegerät' },
];
const results = await classifyBatch(client, items);Wann Batch verwenden
| Szenario | Empfohlenes Muster |
|---|---|
| 1 bis 5 Produkte | Einzelanfragen parallel |
| 6 bis 100 Produkte | Batch-Endpunkt |
| 100+ Produkte | Batch mit Paginierung (Batches zu 100) |
| Echtzeit (1 Produkt) | Einzelanfrage |
Fehlerbehandlung
HTTP-Fehlercodes
| Code | Bedeutung | Empfohlene Aktion |
|---|---|---|
| 400 | Ungültige Anfrage | Datenformat prüfen |
| 401 | Ungültiger API-Schlüssel | API-Schlüssel prüfen |
| 403 | Zugriff verweigert | Organisationsberechtigungen prüfen |
| 404 | Ressource nicht gefunden | Job-Kennung prüfen |
| 429 | Ratenbegrenzung überschritten | Warten und erneut versuchen (siehe Header) |
| 500 | Serverfehler | Mit Backoff erneut versuchen |
| 503 | Dienst nicht verfügbar | Nach einigen Sekunden erneut versuchen |
Retry-Logik
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;
// Keine Wiederholung bei Client-Fehlern (4xx außer 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;
}
// Verwendung
const result = await withRetry(() => client.classify('Mein Produkt'));Ratenbegrenzung
Die API wendet Anfragelimits an, um die Servicequalität sicherzustellen. Limits werden über Antwort-Header kommuniziert.
Ratenbegrenzungs-Header
| Header | Beschreibung |
|---|---|
X-RateLimit-Limit | Maximale Anzahl von Anfragen pro Zeitfenster (pro Bucket) |
X-RateLimit-Remaining | Verbleibende Anfragen im aktuellen Zeitfenster |
Retry-After | Sekunden bis zum nächsten Versuch (nur bei 429) |
Getrennte Buckets
Die API verwendet zwei unabhängige Buckets pro API-Schlüssel:
| Bucket | HTTP-Methoden | Limit | Begründung |
|---|---|---|---|
| Write | POST, PUT, PATCH, DELETE | 10/min | LLM-Operationen (jede SSE bleibt ~90s offen) |
| Read | GET, HEAD, OPTIONS | 60/min | Status-Polling (alle 2s = typisch 30/min) |
Ein Batch-Aufruf zählt als eine Schreibanfrage. Das Limit pro Schlüssel kann im Entwickler-Dashboard reduziert werden.
Throttling-Implementierung
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) {
// Feste 1-Minuten-Periode – warten bis Reset
await new Promise((r) => setTimeout(r, 60_000));
this.remaining = this.limit;
}
this.remaining--;
}
}Abrechnung
Die API verwendet ein Pay-per-Use-Modell mit gestaffelten Preisen. Es gibt keine festen Pläne. Details finden Sie in der vollständigen Dokumentation.
Webhook (bald verfügbar)
Das Webhook-Muster ermöglicht es Ihnen, Ergebnisse direkt auf Ihrem Server zu empfangen, ohne Polling.
// Webhook registrieren
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',
}),
});
// Ihr Empfangs-Endpunkt
app.post('/webhooks/tradehub', (req, res) => {
// Signatur prüfen
const signature = req.headers['x-tradehub-signature'];
const isValid = verifySignature(req.body, signature, webhookSecret);
if (!isValid) {
return res.status(401).send('Ungültige Signatur');
}
const event = req.body;
if (event.type === 'classification.completed') {
processResult(event.data.job_id, event.data.result);
}
res.status(200).send('OK');
});Best Practices für die Produktion
- Immer Wiederholungen mit exponentiellem Backoff implementieren für 5xx- und 429-Fehler
- Ratenbegrenzungs-Header beachten, um Blockierungen zu vermeiden
- Batch verwenden bei mehr als 5 gleichzeitigen Klassifizierungen
- Ergebnisse lokal cachen, um doppelte Klassifizierungen zu vermeiden
- Fehler protokollieren, um Fehler-Muster zu erkennen
- Timeouts auf Client-Seite verwenden (empfohlen: 30 Sekunden pro Anfrage)
- Eingaben validieren vor dem Senden (nicht-leere Beschreibung, gültiger Ländercode)
- Teilresultate bei Batch verarbeiten (einige Einträge können fehlschlagen)
Last updated on
Exportablauf – API-Integration
Wie Sie die The Trade Hub API in einem Exportablauf verwenden – Klassifizierung, Exportkontrolle und bewährte Verfahren.
Neuer französischer Zollkodex 2026 – Vollständiger Recodifizierungsleitfaden
Referenzleitfaden zum neuen französischen Zollkodex (Code des douanes), recodifiziert durch die Ordonnance 2026-265. 7-Bände-Struktur, Artikel-für-Artikel-Konkordanz mit dem alten Kodex, materielle Änderungen, praktische Auswirkungen nach Berufsgruppen. In Kraft ab 1. Mai 2026.