Patrones de integración
Buenas prácticas para integrar la API de The Trade Hub en producción - polling, batch, gestión de errores y rate limiting.
Esta guía presenta los patrones recomendados para integrar la API de The Trade Hub en sus aplicaciones de producción. Encontrará ejemplos concretos en TypeScript y Python.
Arquitectura de la API
La API de The Trade Hub utiliza un modelo asíncrono por jobs para las operaciones de clasificación. Esta elección arquitectónica permite procesar solicitudes complejas sin bloquear al cliente.
Cliente ──POST /classify──> API ──> Job creado
Cliente <── job_id ────────────────────────┘
Cliente ──GET /classify/:id──> API ──> Estado del job
Cliente <── status + result ──────────────────────┘Patrón de polling
El polling es el patrón principal para recuperar los resultados de clasificación.
Implementación recomendada
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. Crear el job
const { job_id } = await this.request<{ job_id: string }>('/classify', {
method: 'POST',
body: JSON.stringify({
content,
origin_country: options?.originCountry,
}),
});
// 2. Polling con backoff exponencial
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}`);
}
// Backoff exponencial con jitter
await new Promise((resolve) =>
setTimeout(resolve, delay + Math.random() * 200)
);
delay = Math.min(delay * 1.5, 5000); // Máx. 5 segundos entre intentos
}
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. Crear el 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. Polling con backoff exponencial
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')}")
# Backoff exponencial con 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")Procesamiento por lotes (batch)
Para clasificar un gran número de productos, utilice el endpoint batch que optimiza el procesamiento del lado del servidor.
interface BatchItem {
id: string; // Su identificador interno
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 a 1
}
async function classifyBatch(
client: TradeHubClient,
items: BatchItem[]
): Promise<BatchResponse> {
// 1. Enviar el batch
const { batch_id } = await client.request<{ batch_id: string }>(
'/classify/batch',
{
method: 'POST',
body: JSON.stringify({ items }),
}
);
// 2. Polling del batch (intervalos más largos)
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(`Progreso del batch: ${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');
}
// Uso
const items: BatchItem[] = [
{ id: 'SKU-001', content: 'Cable USB-C a Lightning' },
{ id: 'SKU-002', content: 'Funda protectora de silicona para iPhone 15' },
{ id: 'SKU-003', content: 'Cargador inalámbrico Qi 15W' },
];
const results = await classifyBatch(client, items);Cuándo usar batch
| Escenario | Patrón recomendado |
|---|---|
| 1 a 5 productos | Solicitudes individuales en paralelo |
| 6 a 100 productos | Endpoint batch |
| 100+ productos | Batch con paginación (lotes de 100) |
| Tiempo real (1 producto) | Solicitud individual |
Gestión de errores
Códigos de error HTTP
| Código | Significado | Acción recomendada |
|---|---|---|
| 400 | Solicitud inválida | Verificar el formato de los datos |
| 401 | Clave API inválida | Verificar la clave API |
| 403 | Acceso denegado | Verificar los permisos de la organización |
| 404 | Recurso no encontrado | Verificar el identificador del job |
| 429 | Límite de tasa excedido | Esperar y reintentar (ver headers) |
| 500 | Error del servidor | Reintentar con backoff |
| 503 | Servicio no disponible | Reintentar después de unos segundos |
Lógica 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;
// No reintentar errores del cliente (4xx excepto 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;
}
// Uso
const result = await withRetry(() => client.classify('Mi producto'));Rate limiting
La API aplica límites de solicitudes para garantizar la calidad del servicio. Los límites se comunican a través de los headers de respuesta.
Headers de rate limiting
| Header | Descripción |
|---|---|
X-RateLimit-Limit | Número máximo de solicitudes por ventana (por bucket) |
X-RateLimit-Remaining | Solicitudes restantes en la ventana actual |
Retry-After | Segundos a esperar (solo en 429) |
Buckets separados
La API utiliza dos buckets independientes por clave API:
| Bucket | Métodos HTTP | Límite | Razón |
|---|---|---|---|
| Escritura | POST, PUT, PATCH, DELETE | 10/min | Operaciones LLM (cada SSE permanece abierto ~90s) |
| Lectura | GET, HEAD, OPTIONS | 60/min | Polling de estado (cada 2s = 30/min típico) |
Una llamada batch cuenta como una sola solicitud de escritura. El límite por clave puede reducirse desde el panel de desarrollador.
Implementación del 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) {
// Ventana fija de 1 minuto - esperar el reinicio
await new Promise((r) => setTimeout(r, 60_000));
this.remaining = this.limit;
}
this.remaining--;
}
}Facturación
La API utiliza un modelo pay-per-use con tarificación graduada. No hay planes fijos. Consulte la documentación completa para más detalles.
Webhook (próximamente)
El patrón webhook permitirá recibir los resultados directamente en su servidor sin necesidad de hacer polling.
// Registrar un webhook
await client.request('/webhooks', {
method: 'POST',
body: JSON.stringify({
url: 'https://api.sudominio.com/webhooks/tradehub',
events: ['classification.completed', 'classification.failed'],
secret: 'whsec_su_secreto_de_verificacion',
}),
});
// Su endpoint de recepción
app.post('/webhooks/tradehub', (req, res) => {
// Verificar la firma
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');
});Buenas prácticas en producción
- Siempre implementar retry con backoff exponencial para errores 5xx y 429
- Respetar los headers de rate limiting para evitar bloqueos
- Usar batch para más de 5 clasificaciones simultáneas
- Almacenar los resultados en caché local para evitar reclasificar los mismos productos
- Registrar los errores para detectar patrones de fallo
- Usar timeouts del lado del cliente (recomendado: 30 segundos por solicitud)
- Validar las entradas antes de enviar las solicitudes (descripción no vacía, código de país válido)
- Gestionar resultados parciales en el batch (algunos items pueden fallar)
Última actualización
Flujo de exportacion - Integracion API
Como utilizar la API The Trade Hub en un flujo de exportacion - clasificacion, control de exportaciones y buenas practicas.
Nuevo Codigo de Aduanas Frances 2026 - Guia completa de la recodificacion
Guia de referencia sobre el nuevo Codigo de Aduanas frances (Code des douanes) recodificado por la ordenanza 2026-265. Estructura en 7 libros, concordancia articulo por articulo con el antiguo codigo, cambios sustantivos, impactos practicos por profesion. En vigor el 1 de mayo de 2026.