Guida al Batch Processing e Callback - Skuno API

📋 Panoramica

Il sistema di batch processing di Skuno permette di inviare più prodotti contemporaneamente per l'arricchimento AI, ricevendo callback individuali per ogni prodotto completato. Questa guida fornisce tutti i dettagli tecnici per implementare correttamente l'integrazione.

🔑 Autenticazione

Skuno utilizza l'autenticazione tramite API Key per tutte le integrazioni.

POST /functions/v1/products-batch
Content-Type: application/json
apikey: your_api_key_here

L'API Key garantisce sicurezza e semplicità d'uso per tutte le operazioni di batch processing.


🚀 Endpoint Batch Processing

Invio Batch di Prodotti

Endpoint: POST /functions/v1/products-batch

Request Headers

Content-Type: application/json
apikey: your_api_key_here

⚠️ Aggiornamento Importante: Campo Language Obbligatorio

A partire da questa versione, ogni prodotto deve includere il campo language nello schema_override. Questo campo specifica la lingua in cui l'AI deve arricchire il prodotto.

Lingue supportate:

  • en - Inglese
  • fr - Francese
  • it - Italiano
  • es - Spagnolo

?> Nota: se il campo language non viene specificato o contiene un valore non supportato, la richiesta verrà rifiutata con un errore di validazione.

🖼️ Gestione Immagini

Richiesta Immagini

Per richiedere anche l'arricchimento automatico delle immagini durante il processo di batch, aggiungi require_images: true all'interno dello schema_override di ogni prodotto:

{
  "barcode": "8001234567890",
  "brand": "Nike",
  "schema_override": {
    "type": "object",
    "properties": {
      "language": "it",
      "require_images": true,
      "product_name": {"type": "string"}
    },
    "required": ["language", "product_name"]
  }
}

Callback con Immagini

Quando l'arricchimento (inclusa la ricerca immagini) è completato, riceverai la callback con il campo images popolato:

{
  "event": "product.enriched",
  "product": {
    "barcode": "8001234567890",
    "enriched_description": "...",
    "images": [
      {
        "image_url": "https://example.com/image.jpg",
        "thumbnail_url": "https://example.com/thumb.jpg",
        "title": "Titolo Immagine",
        "is_primary": true
      }
    ],
    "image_status": "completed",
    "images_found": 1
  }
}

Esempio di Request Body Completo

{
  "callback_url": "https://your-domain.com/webhook/skuno",
  "products": [
    {
      "barcode": "8001234567890",
      "brand": "Nike",
      "current_description": "Scarpe da running Air Max",
      "schema_override": {
        "type": "object",
        "properties": {
          "language": "it",
          "product_name": {"type": "string"},
          "description": {"type": "string"},
          "brand": {"type": "string"}
        },
        "required": ["product_name", "brand"]
      }
    },
    {
      "barcode": "8001234567891",
      "brand": "Adidas",
      "current_description": "Maglietta sportiva Climacool",
      "schema_override": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "enum": ["en", "fr", "it", "es"],
            "description": "Lingua per l'arricchimento del prodotto"
          },
          "product_name": {"type": "string"},
          "description": {"type": "string"},
          "brand": {"type": "string"}
        },
        "required": ["product_name", "brand"]
      }
    },
    {
      "barcode": "8001234567892",
      "brand": "Puma",
      "current_description": "Pantaloni da allenamento",
      "schema_override": {
        "type": "object",
        "properties": {
          "language": {
            "type": "string",
            "enum": ["en", "fr", "it", "es"],
            "description": "Lingua per l'arricchimento del prodotto"
          },
          "product_name": {"type": "string"},
          "description": {"type": "string"},
          "brand": {"type": "string"}
        },
        "required": ["product_name", "brand"]
      }
    }
  ]
}

Parametri

| Campo | Tipo | Obbligatorio | Descrizione | |------|------|--------------|-------------| | callback_url | string | Sì | URL dove ricevere le callback di completamento | | products | array | Sì | Array di prodotti da processare | | products[].barcode | string | Sì | Codice a barre univoco del prodotto | | products[].brand | string | Sì | Marca del prodotto | | products[].current_description | string | Sì | Descrizione attuale del prodotto | | products[].schema_override | object | Sì | Schema personalizzato per l'arricchimento AI | | products[].schema_override.properties.language | string | Sì | Lingua per l'arricchimento (en, fr, it, es) |

Response di Successo (201)

{
  "success": true,
  "batch_id": "b324448f-e99b-44c5-a9c6-566f880e1aa1",
  "total_products": 3,
  "created": 3,
  "failed": 0,
  "errors": [],
  "message": "Batch processing completed. Created: 3, Failed: 0"
}

Response di Errore (400/401/500)

{
  "success": false,
  "error": "Authentication failed",
  "message": "Missing authorization header"
}

📡 Sistema di Callback

Comportamento delle Callback

  • IMPORTANTE: il sistema invia una callback per ogni prodotto processato, non una callback unica per l'intero batch.
  • 3 prodotti inviati = 3 callback ricevute
  • 10 prodotti inviati = 10 callback ricevute
  • Ogni callback viene inviata quando l'arricchimento AI di un singolo prodotto è completato.

Formato Callback

Ogni callback è una richiesta HTTP POST al vostro callback_url:

POST https://your-domain.com/webhook/skuno
Content-Type: application/json
User-Agent: Skuno-Webhook/1.0
{
  "event": "product.enriched",
  "batch_id": "b324448f-e99b-44c5-a9c6-566f880e1aa1",
  "product_id": "550e8400-e29b-41d4-a716-446655440000",
  "timestamp": "2025-01-25T14:30:00.000Z",
  "product": {
    "barcode": "8001234567890",
    "brand": "Nike",
    "original_description": "Scarpe da running Air Max",
    "enriched_description": "Scarpe da running Nike Air Max con tecnologia di ammortizzazione avanzata, ideali per corridori che cercano comfort e performance. Suola in gomma resistente e tomaia traspirante.",
    "enriched_data": {
      "tags": ["running", "sport", "scarpe", "nike", "air-max"],
      "category": "Calzature Sportive",
      "features": [
        "Ammortizzazione Air Max",
        "Tomaia traspirante",
        "Suola in gomma resistente"
      ],
      "target_audience": "Corridori e appassionati di fitness",
      "confidence_score": 0.95
    },
    "images": [
      {
        "image_url": "https://example.com/image1.jpg",
        "thumbnail_url": "https://example.com/thumb1.jpg",
        "title": "Nike Air Max Lato",
        "source_url": "https://source.com/page",
        "width": 800,
        "height": 600,
        "is_primary": true,
        "relevance_score": 8.5,
        "quality_score": 9.0
      }
    ],
    "image_status": "completed",
    "images_found": 1
  },
  "processing_info": {
    "started_at": "2025-01-25T14:29:45.000Z",
    "completed_at": "2025-01-25T14:30:00.000Z",
    "processing_time_ms": 15000
  }
}

Campi della Callback

| Campo | Tipo | Descrizione | |------|------|-------------| | event | string | Sempre product.enriched | | batch_id | string | ID del batch originale | | product_id | string | ID univoco del prodotto | | timestamp | string | Timestamp ISO 8601 del completamento | | product.barcode | string | Codice a barre originale | | product.brand | string | Marca originale | | product.original_description | string | Descrizione originale | | product.enriched_description | string | Descrizione arricchita dall'AI | | product.enriched_data | object | Dati aggiuntivi generati dall'AI | | product.images | array | Array di oggetti contenenti le URL e metadata delle immagini del prodotto (se richieste) | | product.image_status | string | Stato della ricerca immagini (completed, failed, no_images_found) | | product.images_found | number | Numero totale di immagini trovate | | processing_info | object | Informazioni sui tempi di elaborazione |


💻 Implementazione Endpoint Callback

Esempio Node.js/Express

const express = require('express');
const app = express();

app.use(express.json());

// Endpoint per ricevere le callback di Skuno
app.post('/webhook/skuno', (req, res) => {
  try {
    const callback = req.body;

    // Validazione base
    if (callback.event !== 'product.enriched') {
      return res.status(400).json({ error: 'Unknown event type' });
    }

    // Log della callback ricevuta
    console.log(`Prodotto arricchito ricevuto:`);
    console.log(`- Batch ID: ${callback.batch_id}`);
    console.log(`- Product ID: ${callback.product_id}`);
    console.log(`- Barcode: ${callback.product.barcode}`);
    console.log(`- Brand: ${callback.product.brand}`);

    // Processa il prodotto arricchito
    processEnrichedProduct(callback);

    // Risposta di successo (IMPORTANTE!)
    res.status(200).json({
      status: 'received',
      timestamp: new Date().toISOString()
    });

  } catch (error) {
    console.error('Errore processing callback:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

function processEnrichedProduct(callback) {
  // Salva nel database, aggiorna catalogo, invia notifiche, ecc.
  const { product, batch_id } = callback;

  updateProductInDatabase({
    barcode: product.barcode,
    enriched_description: product.enriched_description,
    tags: product.enriched_data.tags,
    category: product.enriched_data.category,
    images: product.images,
    batch_id: batch_id
  });
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Esempio PHP

<?php
// webhook.php

header('Content-Type: application/json');

$input = file_get_contents('php://input');
$callback = json_decode($input, true);

if (!$callback) {
  http_response_code(400);
  echo json_encode(['error' => 'Invalid JSON']);
  exit;
}

if ($callback['event'] !== 'product.enriched') {
  http_response_code(400);
  echo json_encode(['error' => 'Unknown event type']);
  exit;
}

try {
  error_log("Skuno callback ricevuta: " . $input);
  $product = $callback['product'];
  $batchId = $callback['batch_id'];

  updateProduct([
    'barcode' => $product['barcode'],
    'enriched_description' => $product['enriched_description'],
    'tags' => json_encode($product['enriched_data']['tags']),
    'batch_id' => $batchId
  ]);

  http_response_code(200);
  echo json_encode([
    'status' => 'received',
    'timestamp' => date('c')
  ]);
} catch (Exception $e) {
  error_log("Errore processing callback: " . $e->getMessage());
  http_response_code(500);
  echo json_encode(['error' => 'Internal server error']);
}
?>

Esempio Python/Flask

from flask import Flask, request, jsonify
from datetime import datetime
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

@app.route('/webhook/skuno', methods=['POST'])
def skuno_webhook():
    try:
        callback = request.get_json()
        if not callback or callback.get('event') != 'product.enriched':
            return jsonify({'error': 'Invalid event type'}), 400

        app.logger.info(f"Callback ricevuta per batch {callback['batch_id']}")
        app.logger.info(f"Prodotto: {callback['product']['barcode']}")

        process_enriched_product(callback)

        return jsonify({
            'status': 'received',
            'timestamp': datetime.utcnow().isoformat() + 'Z'
        }), 200

    except Exception as e:
        app.logger.error(f"Errore processing callback: {str(e)}")
        return jsonify({'error': 'Internal server error'}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)

🔒 Sicurezza e Best Practices

1) Validazione delle Callback

function validateCallback(callback) {
  const required = ['event', 'batch_id', 'product_id', 'product'];
  for (const field of required) {
    if (!callback[field]) {
      throw new Error(`Missing required field: ${field}`);
    }
  }
  if (callback.event !== 'product.enriched') {
    throw new Error('Invalid event type');
  }
  return true;
}

2) Gestione degli Errori

app.post('/webhook/skuno', (req, res) => {
  try {
    validateCallback(req.body);
    processCallback(req.body);
    res.status(200).json({ status: 'received' });
  } catch (error) {
    console.error('Callback error:', error);
    res.status(400).json({ error: error.message });
  }
});

3) Idempotenza

const processedCallbacks = new Set();
function processCallback(callback) {
  const callbackId = `${callback.batch_id}-${callback.product_id}`;
  if (processedCallbacks.has(callbackId)) return;
  updateProduct(callback.product);
  processedCallbacks.add(callbackId);
}

4) Timeout e Retry

  • Skuno ritenta le callback fallite fino a 3 volte
  • Timeout di 30 secondi per ogni tentativo
  • Intervalli di retry: 1min, 5min, 15min

📊 Monitoraggio e Tracking

const activeBatches = new Map();
function onBatchCompleted(batchId, batch) {
  const duration = new Date() - batch.started_at;
  console.log(`Batch ${batchId} completato in ${duration}ms`);
}

🧪 Test e Sviluppo

Test con LocalTunnel

npm install -g localtunnel
node your-webhook-server.js
lt --port 3000

Usa l'URL fornito come callback_url (es: https://abc123.loca.lt).

Esempio di Test

const fetch = require('node-fetch');
async function testBatch() { /* ... come da guida ... */ }
testBatch();

❓ FAQ

  • Quante callback riceverò per un batch di N prodotti? N callback, una per prodotto.
  • Le callback arrivano in ordine? No, l'elaborazione è parallela.
  • Cosa succede se il mio endpoint è offline? Retry fino a 3 volte (1m, 5m, 15m).
  • Posso avere callback al completamento del batch? Non ancora; traccia le callback ricevute lato client.
  • Come gestisco callback duplicate? Usa product_id e logica di idempotenza.
  • Timeout per le callback? 30 secondi per ogni tentativo.