Documentazione pubblica / partner Questa versione è pensata per clienti e partner che integrano le API. Alcune sezioni riservate agli amministratori (gestione utenti, tier, dettagli infrastruttura) non sono incluse.

Base URL & ambienti

Gli endpoint sono versionati sotto /api/v1. Usa sempre HTTPS.

Ambiente Base URL Note
Produzione https://flow.icarry.it Connesso alla flotta reale e ai job schedulati da iCarryUP.

In questa versione (partner) i link a Swagger e ReDoc non sono mostrati. L'accesso a /api/docs, /api/redoc e /openapi.json è riservato ai soli superadmin (altri account ricevono 403). Per i partner fa fede l'elenco endpoint di questa pagina.

Autenticazione

L'API utilizza token JWT con schema Bearer. Ottieni il token inviando credenziali al token endpoint OAuth2.

POST /api/v1/auth/token
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=OPS_USER&password=********

Il token restituito va inviato nell'header:

Authorization: Bearer <ACCESS_TOKEN>

⚠️ Imposta una rotazione token server-side ogni 24h o utilizza refresh programmati. Per integrazioni server-to-server è consigliato creare utenti tecnici dedicati (ruolo: system).

Workflow consigliato

  1. Carica i dati logistici (driver + consegne) nel formato previsto da RoutingOptimizationRequest.
  2. Invia POST /api/v1/routing/optimize con async_processing=true; ricevi un job_id.
  3. Monitora lo stato via GET /api/v1/routing/jobs/{job_id} (polling 5-10s) o via WebSocket /ws/jobs.
  4. Quando status=completed leggi result, esporta file o genera mappe con gli endpoint dedicati.
  5. Optional: imposta webhook_url nella richiesta per ricevere callback push (payload identico a result).

Endpoints critici

POST /api/v1/routing/optimize

Sottomette una nuova ottimizzazione. Supporta sia payload moderni sia legacy (conversione automatica di lat/lng in start_lat, ecc.).

{
  "async_processing": true,
  "session_id": "tms-session-example-001",
  "webhook_url": "https://your-tms.example.com/webhooks/routing",
  "delivery_points": [
    {
      "tracking_number": "SHIP-EXAMPLE-0001",
      "delivery_company": "Example SpA",
      "address": "Via Esempio 1, Milano",
      "lat": 45.4642,
      "lng": 9.1900,
      "tot_items": 4,
      "priority": 1
    }
  ],
  "drivers": [
    {
      "name": "Driver example 01",
      "start_lat": 45.5342,
      "start_lng": 9.3265,
      "end_lat": 45.5342,
      "end_lng": 9.3265,
      "capacity": 90,
      "vehicle_type": "van_standard",
      "max_distance_km": 220
    }
  ],
  "algorithms": ["standard"],
  "optimization_objective": "time",
  "optimization_params": {
    "max_iterations": 120,
    "time_limit_seconds": 420,
    
    "distance_weight": 0.5,
    "time_weight": 0.3,
    "balance_weight": 0.2
    
  },
  "constraints": {
    "max_route_distance": 250,
    "max_route_duration": 480,
    "max_stops_per_route": 45
  }
}

Risposta immediata:

{
  "job_id": "6b758bee-65e5-473e-add8-8cecc36f6fda",
  "status": "queued",
  "estimated_completion": "2025-11-26T09:11:37.123Z",
  "result_url": "/api/v1/routing/jobs/6b758bee-65e5-473e-add8-8cecc36f6fda"
}

GEL (pickup locker): se l’utente non ha la feature di piano gel_proximity (tenant con GEL su mappa + flag sul tier/profilo), il backend rimuove silenziosamente pickup_point_id dalle consegne e gel_cost_to_time_equivalent_minutes_per_eur da optimization_params prima dell’ottimizzazione.

Quale obiettivo scegliere

Gli stessi quattro valori del wizard in console. Il campo è optimization_objective su POST /api/v1/routing/optimize. Il default è time. Un valore sconosciuto viene trattato come time. Esistono perché un solo numero non può essere insieme il giro più corto, quello più puntuale e quello più economico.

Meno tempo time

PerchéSenza questo obiettivo un giro corto in km può far aspettare i destinatari. Penalizza l’attesa e riempie le fasce orarie.

QuandoUsalo con SLA, finestre strette o promesse orarie.

Meno km distance

PerchéServe quando conta il chilometraggio, non l’orologio. L’attesa non è penalizzata.

QuandoUsalo se dominano carburante e pedaggi, zone sparse o poche fasce orarie.

Bilanciato balanced

PerchéCompromesso tra tempo e chilometri, senza spingere un estremo.

QuandoUsalo per una flotta mista o una giornata normale.

Meno costi cost

PerchéConverte chilometri e ore in euro con i campi del driver: hourly_cost, fuel_consumption, prezzo_litro, costo_pedaggio_km.

QuandoUsalo per un budget di flotta o un confronto economico. budget_max_eur in optimization_params è il tetto, non questo obiettivo.

Non è balance_options: quello spartisce il carico tra i fattorini (stesso numero di tappe, km o minuti). I pesi distance_weight, time_weight e balance_weight in optimization_params sono una taratura fine; per l’integrazione basta optimization_objective.

{
  "optimization_objective": "time"
}

Sostituisci time con distance, balanced o cost. Gli altri campi della richiesta restano quelli dell’esempio optimize.

GET /api/v1/routing/jobs/{job_id}

Restituisce stato, progress e, se completato, l'intero RoutingResultResponse (rotte, geometrie, metriche globali). I risultati sono persistiti in cache condivisa ed eliminati durante la manutenzione programmata.

Nel risultato completato può essere presente il campo savings_report: stima di risparmio (km/tempo/costo) rispetto a una baseline nearest-neighbor sullo stesso job.

GET /api/v1/routing/jobs

Lista gli ultimi job dell'utente corrente. Accetta ?limit=50 e ?status=processing/completed... per implementare dashboard operative.

DELETE /api/v1/routing/jobs/{job_id}

Annulla job in coda o in esecuzione. Se il lavoro è già concluso restituisce errore 400.

GET /api/v1/routing/eta

Stima la durata wall-clock dell'ottimizzazione (secondi) a partire da run storici simili (P50/OLS) o dal budget TSP. Query tipiche: deliveries, drivers, algorithm, routing_provider, has_time_windows, pickup_count, optimization_objective.

GET /api/v1/routing/history

Cronologia delle ottimizzazioni persistenti dell'utente/tenant (esclude le simulazioni se richiesto). Supporta q= per cercare per nome o job_id. Utile per dashboard e audit.

GET /api/v1/routing/calendar

Eventi calendario (run pianificati e bozze) per un intervallo di date. Alimenta la vista calendario della console.

GET /api/v1/routing/status

Health-check arricchito (jobs attivi, versione, uptime). Può essere usato per sistemi di monitoraggio o readiness probe.

Il campo algorithms_available è riservato agli account amministrativi; per gli altri è sempre un array vuoto.

POST /api/v1/routing/simulate

Avvia una simulazione what-if (Control Tower) a partire da un job baseline. Restituisce un simulation_id; il risultato è un job di ottimizzazione marcato is_simulation=true.

{
  "baseline_job_id": "6b758bee-65e5-473e-add8-8cecc36f6fda",
  "baseline_scope": "PLANNED",
  "simulation_mode": "reoptimize",
  "patches": [
    {
      "type": "add_delivery",
      "point_type": "delivery",
      "lat": 41.9028,
      "lng": 12.4964,
      "tracking_number": "NEW-001",
      "pickup_point_id": "HUB-1"
    },
    { "type": "remove_delivery", "tracking_number": "OLD-042" }
  ]
}

Patch supportate: add_delivery (anche ritiro con point_type=pickup e pickup_point_id), remove_delivery, add_driver, remove_driver, delivery_time_window, balance_options, invalidate_matrices. baseline_scope=PLANNED (LIVE non supportato in v1).

GET /api/v1/routing/simulations/{simulation_id}

Restituisce metadati e confronto della simulazione (baseline vs risultato what-if).

POST /api/v1/routing/estimate-workload

Stima il fabbisogno di lavoro (minuti/driver) prima di scegliere la flotta. mode=quick (euristica rapida) o precise (solver con flotta virtuale).

GET /api/v1/routing/delivery-execution/summary

KPI di esecuzione consegne (es. puntualità %) aggregato sugli ultimi job dell'utente.

GET /api/v1/routing/jobs/{job_id}/delivery-execution

Dettaglio esecuzione consegne e prove di consegna (POD) per un singolo job.

POST /api/v1/routing/manual-assignments

Permette di fissare consegne → driver prima di lanciare l'ottimizzazione. Gli altri endpoint GET/DELETE /manual-assignments consentono lettura/reset delle preferenze.

POST /api/v1/routing/jobs/{job_id}/generate-real-map

Crea mappe HTML persistenti (cartella web/static/maps) pronte per la distribuzione ai driver. GET .../download-map e GET .../export generano rispettivamente mappe e Excel.

POST /api/v1/routing/jobs/{job_id}/generate-geometries

Genera le geometrie stradali per un job completato (JWT obbligatorio). Header opzionale If-Match con routes_version_hash; mismatch → 409 routes_version_mismatch — ritentare senza header (le geometrie arrivano dal job salvato).

GET /api/v1/routing/driver-maps

Elenco mappe driver del tenant corrente (JWT obbligatorio; 401 senza token). Superuser senza impersonazione può vedere tutti i tenant.

Geocoding (autenticato)

JWT obbligatorio. POST /optimize non geocodifica: lat/lng nel JSON restano obbligatori. Usa questi endpoint prima. source è un’etichetta diagnostica del resolver, non un contratto.

POST /api/v1/geocoding/lookup

Solo cache, massimo 500 indirizzi. hit false = non in cache (usa forward). Non avvia una risoluzione esterna.

{
  "addresses": ["Via Roma 1, Milano", "Piazza Duomo, Firenze"]
}
{
  "results": [
    { "index": 0, "hit": true, "lat": 45.46, "lng": 9.19, "source": "…", "low_confidence": false, "cache_id": 12 },
    { "index": 1, "hit": false, "lat": null, "lng": null, "source": null, "low_confidence": null, "cache_id": null }
  ]
}

POST /api/v1/geocoding/forward

Risolve fino a 10 indirizzi (raw + query). origin: api (default), wizard, mobile, one_driver, simulator, rubrica. Budget circa 25 secondi. status: ok, not_found, pending (ripeti la stessa chiamata), error.

{
  "origin": "api",
  "items": [{ "raw": "Via Roma 1, Milano", "query": "Via Roma 1, Milano" }]
}
{
  "results": [
    {
      "status": "ok",
      "lat": 45.46,
      "lng": 9.19,
      "confidence": 90,
      "matched": true,
      "low_confidence": false,
      "source": "…",
      "cache_id": 12
    }
  ]
}

GET /api/geocode_address?address=

Un solo indirizzo, stesso resolver di forward. 200 con lat/lng; 400 se manca address; 404 se nessun risultato; 503 se la risoluzione è ancora pending.

GET /api/reverse_geocode?lat=&lng=

Da coordinate a indirizzo. 200 { address, source }; 400 se manca lat o lng; 404 se nessun risultato.

Pin coordinate Enterprise

Piano Enterprise: salva indirizzi e coordinate. I pin del tenant hanno precedenza in lookup/forward. La cattura automatica da POST /optimize è opzionale (settings).

POST /api/v1/geocoding/pins

Upsert fino a 500 pin. Stesso ruolo di DELETE e PUT settings: owner, admin o superuser, oltre alla feature geocode_pins. Un member riceve 403 owner_or_admin_required. Ogni elemento di results ha index, status, pin_id, address_key, error.

status: created (nuovo), updated (stesso indirizzo, coordinate spostate di almeno 5 m), unchanged (spostamento sotto i 5 m), rejected (error: invalid_address, invalid_coordinates, zero_coordinates, swapped_lat_lng, out_of_area), limit_reached (tetto pin del tenant). summary usa gli stessi conteggi.

address_key è la stessa normalizzazione di lookup e forward: Unicode NFKC, minuscolo, spazi compressi, virgole rese come «, », spazi e virgole di bordo tolti, massimo 500 caratteri. La stessa stringa su POST e su lookup trova il pin: usate il valore restituito. Cambiano la chiave civico, CAP o parole in più; non la cambiano maiuscole o spazi doppi.

{
  "items": [
    { "address": "Via Roma 1, 20121 Milano MI", "lat": 45.46, "lng": 9.19, "external_id": "CLI-1" }
  ]
}
{
  "results": [
    { "index": 0, "status": "created", "pin_id": 12, "address_key": "via roma 1, 20121 milano mi", "error": null }
  ],
  "summary": { "created": 1, "updated": 0, "unchanged": 0, "rejected": 0, "limit_reached": 0 }
}

GET /api/v1/geocoding/pins

Elenco pin di chi ha la feature (q da 3 caratteri, external_id, limit, after_id).

DELETE /api/v1/geocoding/pins/{pin_id}

Elimina un pin del proprio tenant. Solo owner, admin o superuser. Il pin di un altro tenant risponde 404.

GET / PUT /api/v1/geocoding/pins/settings

GET (feature): enabled, auto_capture, count, max_pins. PUT (owner/admin): corpo obbligatorio { "auto_capture": true|false }. Risposta PUT: enabled, auto_capture, max_pins.

{ "auto_capture": true }

Export Excel (autenticati)

POST /api/v1/routing/export-geocoded-deliveries

Richiede Authorization Bearer. Restituisce un file Excel dal payload inviato.

POST /api/v1/routing/export-geocoded-drivers

Richiede Authorization Bearer. Restituisce un file Excel dal payload inviato.

POST /api/v1/routing/export-manual-drivers

Richiede Authorization Bearer. Restituisce un file Excel dal payload inviato.

POST /api/v1/routing/export-manual-deliveries

Richiede Authorization Bearer. Restituisce un file Excel dal payload inviato.

POST /api/v1/routing/export-manual-pickups

Richiede Authorization Bearer. Restituisce un file Excel dal payload inviato.

Endpoint pubblici / bootstrap

GET /api/v1/public/features

Restituisce flag frontend: gel_proximity_enabled, rlp_bryntum_timeline_enabled e bootstrap PostHog (posthog_enabled, posthog_host, posthog_project_api_key, posthog_session_replay, posthog_unmask_replay). Bearer opzionale: senza token GEL è false; con token si valutano tenant e tier. Se PostHog è disabilitato o incompleto, posthog_enabled è false e la project key non viene esposta.

GET /api/v1/public/fuel-prices

Medie carburante ed energia, senza autenticazione. Non è uno storico iCarry: snapshot corrente da MIMIT (medie giornaliere Italia e per regione) più GNL/HVO da impianti e ricarica pubblica AC/DC mensile. Per UI e stime di costo, non per fatturazione e senza SLA sulle fonti terze. Non è il campo prezzo_litro del giro.

Cache-Control public, max-age=1800: rispettare quel TTL in polling. national e regions: gasolio, benzina e GPL in €/L, metano in €/kg (regions[].name è il nome regione MIMIT, es. Lombardia). extended.gnl (eur_kg), hvo (eur_l), electric_ac/electric_dc (eur_kwh) con value, unit, updated, source. Se tutte le fonti falliscono: {"available": false}. Il PUN GME non è incluso. L’esempio JSON è di forma, non valori live.

{
  "available": true,
  "source": "MIMIT",
  "updated": "21/09/2026",
  "national": { "gasolio": 1.623, "benzina": 1.712, "gpl": 0.741, "metano": 1.401 },
  "regions": [{ "name": "Lombardia", "gasolio": 1.631, "benzina": 1.720, "gpl": 0.738, "metano": 1.390 }],
  "extended": {
    "gnl": { "value": 1.452, "unit": "eur_kg", "source": "mimit_impianti" },
    "hvo": { "value": 1.890, "unit": "eur_l", "source": "mimit_impianti" },
    "electric_ac": { "value": 0.62, "unit": "eur_kwh", "source": "adiconsum_mensile" }
  }
}

L’endpoint per l’elenco punti ritiro GEL sulla mappa non è incluso in questa documentazione partner pubblica. È descritto nella documentazione interna (superadmin) e fornito su richiesta se previsto dal contratto. Contatta il supporto per integrazioni locker.

Dizionario dati di input (Excel/CSV ↔ JSON)

Le colonne ufficiali dei template corrispondono ai campi di POST /api/v1/routing/optimize. Scaricate i template vuoti, poi inviate JSON geocodificato. L’ordine colonne è definito nei template consegne e driver.

Template: GET /api/v1/templates/delivery-template e GET /api/v1/templates/driver-template (XLSX/CSV). Non esiste download pickup separato: usate il foglio consegne con point_type=pickup.

In Excel lat/lng possono mancare se l’indirizzo è geocodificabile. POST /optimize richiede sempre lat/lng numerici (e start_lat/start_lng per i driver).

POST /api/v1/files/upload è solo storage: non fa parse delle colonne né l’ottimizzazione. L’integrazione API partner è JSON verso /optimize.

Consegne / stop (delivery_points[])

Excel JSON /optimize Obbl. Tipo Uso
tracking_numbertracking_numbersìstringa ≤64ID tappa
delivery_addressaddresssì (o lat+lng)stringaGeocoding / display
delivery_companydelivery_companysì in JSON (default import «Cliente»)stringaMetadato
lat / lnglat / lngsì in JSON; opz. in Excel se c’è indirizzogradiMatrice / geometria
tot_itemstot_itemsno (default 1)int ≥1Capacità (colli)
prioritypriorityno (default 1)1–10 (1 = alta)Priorità / drop
estimated_durationestimated_durationnominutiTempo di sosta
notesnotesnostringa ≤500Solo app driver
phonephonenoE.164 (+ e cifre, es. +393331234567)Telefono destinatario (link tel in app e Control Tower)
preassigned_driverpreassigned_drivernodrivers[].nameLock → mandatory_assignments
point_typepoint_typeno (default delivery)pickup | deliveryOrdine ritiro→consegna; pickup senza collegamento = ritiro autonomo
pickup_point_idpickup_point_idnostringaCollegamento ritiro→consegna; assente = ritiro autonomo (rientro in base)
time_window_start / endstessonoHH:MMFinestre orarie
volume_m3 / weight_kgstessonofloat ≥0Cap volume (peso → 0.001 m³ se manca volume)

Solo JSON (punto): id opzionale (altrimenti tracking_number o UUID generato).

Un point_type=pickup senza consegna collegata non viene scartato: è una tappa obbligatoria e la merce rientra al punto di arrivo del fattorino. In risposta il punto ha pickup_mode=return_to_depot e compare una riga in warnings. Se non è pianificabile finisce in unassigned_deliveries.

Driver / veicoli (drivers[])

Excel JSON /optimize Obbl. Note
namenamesìIdentità
phonephonenoTelefono fattorino opzionale in E.164
start_address / end_address—start obbligatorio in importGeocoding → start_* / end_*
lat / lngstart_lat / start_lngsì in JSONDeposito partenza
end_lat / end_lngend_lat / end_lngsì in JSON (default = start)Rientro
capacitycapacityno (default 50)Max colli (tot_items)
max_volume_m3max_volume_m3noCap volume
vehicle_typevehicle_typeno (default car)Elenco completo sotto: car, van, van_standard, van_maxi, truck, truck_7_5t, truck_12t, truck_18t, truck_32t, artic_44t, bike, scooter, icarry_cargo_bike
routing_profilerouting_profilenoOverride: car, van_standard, van_maxi, truck_7_5t, truck_12t, truck_18t, truck_32t, artic_44t, icarry_cargo_bike
max_distance_kmmax_distance_kmnoCap km
hourly_cost, fuel_consumption, prezzo_litro, costo_pedaggio_kmstessonoCosti / report
start_time / end_timestessono (08:00–18:00 se coppia)Turno
break_start / break_endstessoin coppiaPausa
max_minutesmax_minutesnoDurata max (derivabile da turno−pausa)

Solo JSON (driver): id opzionale, active (default true), max_overtime_minutes (default 0).

Tipi di veicolo ammessi (elenco completo)

Si invia uno slug, non il modello commerciale. Default vehicle_type=car. Il 4x4 o l’allestimento non hanno uno slug dedicato.

Slug API Categoria Quando usarlo
carAuto (M1)Auto standard.
van AliasFurgoni (N1)Stesso effetto di van_standard.
van_standardFurgoni (N1)Furgone compatto fino a 2.5 t, altezza circa 1.9 m.
van_maxiFurgoni (N1)Furgone tetto alto fino a 3.5 t, altezza circa 2.7 m.
truck AliasAutocarriStesso effetto di truck_7_5t.
truck_7_5tAutocarriCamion leggero urbano, MMA 7.5 t.
truck_12tAutocarriCamion medio, MMA 12 t. Esempio: Iveco TGM 13.250 4x4 → truck_12t.
truck_18tAutocarriCamion pesante 2 assi, MMA 18 t.
truck_32tAutocarriCamion 4 assi, MMA 32 t.
artic_44tAutoarticolatiBilico 44 t, circa 16.5 m.
bike AliasCiclo-logisticaStesso effetto di icarry_cargo_bike.
scooter AliasAuto (M1)Stesso effetto di car.
icarry_cargo_bikeCiclo-logisticaCargo bike ultimo miglio.

routing_profile, se presente, deve essere uno slug stradale della tabella (non un alias van/truck/bike/scooter) e ha priorità su vehicle_type.

Alias intestazioni frequenti (IT/EN)

L’import file riconosce anche intestazioni alternative (normalizzate, senza accenti). Esempi frequenti:

Schema dati principale

DeliveryPointRequest

Obbligatori in JSON: lat/lng, tracking_number (≤64), address, delivery_company. Opzionali: tot_items (default 1), priority (1–10), estimated_duration (minuti), notes, phone (E.164, es. +393331234567), preassigned_driver, point_type (pickup|delivery), pickup_point_id, time_window_start/end (HH:MM), volume_m3, weight_kg, id.

Mappa completa Excel↔JSON: vedi «Dizionario dati di input» sopra.

DriverRequest

Obbligatori in JSON: name, start_lat/lng, end_lat/lng (end di default = start). Opzionali: phone (E.164), capacity (default 50), max_volume_m3, vehicle_type (default car; elenco completo nella tabella tipi veicolo), routing_profile, max_distance_km, costi (hourly_cost, fuel_consumption, prezzo_litro, costo_pedaggio_km), start_time/end_time, break_start/end, max_minutes, active, max_overtime_minutes, id. Solo Excel: start_address/end_address (geocodificati in coordinate).

vehicle_type — elenco completo: car, van, van_standard, van_maxi, truck, truck_7_5t, truck_12t, truck_18t, truck_32t, artic_44t, bike, scooter, icarry_cargo_bike. Default car. Non si invia marca o modello: un Iveco TGM 13.250 4x4 è truck_12t. Alias: van → van_standard, truck → truck_7_5t, bike → icarry_cargo_bike, scooter → car.

routing_profile — override opzionale. Valori ammessi: car, van_standard, van_maxi, truck_7_5t, truck_12t, truck_18t, truck_32t, artic_44t, icarry_cargo_bike. Se omesso si deriva da vehicle_type.

Mappa completa Excel↔JSON: vedi «Dizionario dati di input» sopra.

OptimizationParamsRequest

Bilancia distanza, tempo e carico con pesi configurabili. I valori dipendono dalle priorità operative.

RoutingConstraintsRequest

Abilita vincoli hard (durata massima, distanza massima, stops per route). Se non definiti il motore applica limiti dinamici basati sui driver.

Webhook di completamento

Imposta webhook_url nella richiesta per ricevere una POST firmata (header X-iCarry-Signature) con lo stesso payload di RoutingResultResponse. Se il webhook restituisce un codice < 300 viene considerato consegnato; in caso contrario vengono effettuati 3 retry esponenziali (15s, 60s, 180s).

Realtime via WebSocket

Connetti wss://flow.icarry.it/ws/jobs e unisciti alla stanza job_{job_id} via evento Socket.IO join_room. Riceverai eventi job_progress, job_completed, job_failed.

Strumenti di supporto

Errori & best practice

Codice Scenario Come reagire
400 Validation error (schema Pydantic) Leggi detail per campo errato; applica sanitizzazione lato chiamante.
401 Token mancante o scaduto (anche driver-maps / export Excel senza Bearer) Rigenera token e ripeti la chiamata.
403 Valore algorithms non consentito (usa auto o standard) Usa auto o standard, oppure contatta il supporto.
403 Endpoint admin/superuser senza privilegi Gli endpoint tenant-admin richiedono is_admin; mutazioni billing/algoritmi e settings richiedono is_superuser.
409 Job duplicato in coda (stesso session_id) Usa sessioni univoche per job paralleli.
409 routes_version_mismatch (If-Match / modifica concorrente delle rotte) Ricarica l’hash del job, oppure ritenta generate-geometries senza If-Match.
503 Dipendenza non configurata (es. token webhook GEL assente) Configura il token/segreto richiesto in admin settings o env, poi riprova.
500 Errore interno / Servizio di routing non raggiungibile Retry con backoff. Verifica stato con /api/v1/routing/status.

Per ridurre i tempi di risposta imposta batch tra 80 e 120 consegne per job; oltre 150 stop valuta il frazionamento automatico (flag allow_partial_assignments true) e ricomposizioni successive.

Supporto

Per funzionalità avanzate (motore high-performance, SLA prioritari, ambienti dedicati) o per attivare IP whitelisting sugli endpoint di produzione contatta [email protected] indicando il tuo client_id e l'IP pubblico da autorizzare.

🚀 Guida Integrazione TMS (Transport Management System)

Questa guida dettagliata spiega come integrare le API di routing e ottimizzazione di iCarryUP con il tuo sistema TMS esistente, senza utilizzare l'interfaccia web. Perfetta per integrazioni server-to-server automatizzate.

Indice Rapido

1. Introduzione all'Integrazione TMS

Perché integrare via API?

Architettura Tipica

TMS System → API iCarryUP → Ottimizzazione → Risultati → TMS System
     ↓              ↓                    ↓              ↓
  Consegne    Autenticazione      Job Queue      Route Ottimizzate
  Driver      JWT Token           Processing     Metriche
  Vincoli     HTTPS               Webhook        Export

Flusso dati: Il tuo TMS invia driver e consegne → iCarryUP ottimizza → Risultati restituiti via API o webhook → Import automatico nel TMS.

Casi d'Uso Comuni

2. Setup e Configurazione Iniziale

2.1 Ottenere Credenziali API

Per utilizzare le API, devi avere un account utente con le credenziali di accesso. Contatta [email protected] per:

2.2 Configurazione Base URL

Configura il base URL in base all'ambiente:

Ambiente Base URL Quando Usare
Produzione https://flow.icarry.it Ambiente di produzione con dati reali

2.3 Variabili d'Ambiente

Configura le seguenti variabili nel tuo sistema:

# Esempio .env o configurazione
ICARRY_API_BASE_URL=https://flow.icarry.it
ICARRY_API_USERNAME=your_username
ICARRY_API_PASSWORD=your_password
ICARRY_WEBHOOK_SECRET=your_webhook_secret  # Opzionale, per verifica webhook
ICARRY_API_TIMEOUT=300  # Timeout in secondi per richieste API

2.4 Verifica Connettività

Testa la connessione all'API con un semplice health check:

# cURL
curl -X GET "https://flow.icarry.it/api/v1/routing/status"

# Risposta tipica (senza Bearer: algorithms_available sempre [])
{
  "status": "healthy",
  "version": "2.9.23-1",
  "active_jobs": 0,
  "total_jobs_processed": 0,
  "algorithms_available": [],
  "routing_service_status": "active",
  "uptime_seconds": 3600.0
}
💡 Nota: L'endpoint /api/v1/routing/status non richiede autenticazione per il reachability check. Il campo algorithms_available è riservato agli account amministrativi; per gli altri è sempre un array vuoto.

2.5 API pubbliche e integrazione GEL (mappa)

Per sapere se le funzionalità di prossimità GEL sono attive per la tua integrazione, usa GET /api/v1/public/features (stessa sezione «Endpoint pubblici» in cima alla pagina). Il Bearer è opzionale: senza token il flag GEL è false; con token valido si valutano tenant (GEL su mappa) e feature gel_proximity sul profilo o sul tier.

Documentazione partner: l’endpoint per l’elenco dei punti ritiro GEL sulla mappa non è incluso in questa versione pubblica. È descritto nella documentazione interna (superadmin) e fornito su richiesta se previsto dal contratto. Contatta [email protected] per integrazioni locker.

Nell’ottimizzazione (POST /api/v1/routing/optimize) puoi usare pickup_point_id sulle consegne e gel_cost_to_time_equivalent_minutes_per_eur in optimization_params. Se l’utente del token non ha gel_proximity sul piano effettivo, il backend rimuove silenziosamente questi campi prima del calcolo: controlla i flag pubblici o il tier prima di fare affidamento sul GEL in ottimizzazione.

3. Autenticazione Dettagliata

3.1 Sistema JWT Bearer Token

L'API utilizza autenticazione JWT (JSON Web Token) con schema Bearer. Il token ha una validità limitata e deve essere rinnovato periodicamente.

3.2 Ottenere il Token di Accesso

Per ottenere un token, invia una richiesta POST all'endpoint di autenticazione:

POST /api/v1/auth/token

Content-Type: application/x-www-form-urlencoded

Parametri:

  • grant_type: Deve essere password
  • username: Il tuo username
  • password: La tua password

3.3 Esempi di Autenticazione

Python (requests)

import requests
from datetime import datetime, timedelta

class iCarryAPIClient:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.token = None
        self.token_expires = None
    
    def authenticate(self):
        """Ottiene un nuovo token di accesso"""
        url = f"{self.base_url}/api/v1/auth/token"
        data = {
            "grant_type": "password",
            "username": self.username,
            "password": self.password
        }
        
        response = requests.post(url, data=data)
        response.raise_for_status()
        
        token_data = response.json()
        self.token = token_data["access_token"]
        # Token valido per 24 ore (default)
        self.token_expires = datetime.now() + timedelta(hours=24)
        
        return self.token
    
    def get_headers(self):
        """Restituisce gli header con il token per le richieste"""
        if not self.token or datetime.now() >= self.token_expires:
            self.authenticate()
        
        return {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }

# Utilizzo
client = iCarryAPIClient(
    base_url="https://flow.icarry.it",
    username="your_username",
    password="your_password"
)
client.authenticate()

JavaScript/Node.js (fetch)

class iCarryAPIClient {
    constructor(baseUrl, username, password) {
        this.baseUrl = baseUrl;
        this.username = username;
        this.password = password;
        this.token = null;
        this.tokenExpires = null;
    }

    async authenticate() {
        const url = `${this.baseUrl}/api/v1/auth/token`;
        const formData = new URLSearchParams();
        formData.append('grant_type', 'password');
        formData.append('username', this.username);
        formData.append('password', this.password);

        const response = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            body: formData
        });

        if (!response.ok) {
            throw new Error(`Authentication failed: ${response.statusText}`);
        }

        const tokenData = await response.json();
        this.token = tokenData.access_token;
        // Token valido per 24 ore
        this.tokenExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
        
        return this.token;
    }

    async getHeaders() {
        if (!this.token || new Date() >= this.tokenExpires) {
            await this.authenticate();
        }

        return {
            'Authorization': `Bearer ${this.token}`,
            'Content-Type': 'application/json'
        };
    }
}

// Utilizzo
const client = new iCarryAPIClient(
    'https://flow.icarry.it',
    'your_username',
    'your_password'
);
await client.authenticate();

cURL

# Ottenere token
curl -X POST "https://flow.icarry.it/api/v1/auth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=your_username&password=your_password"

# Risposta
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 86400
}

# Usare il token nelle richieste successive
curl -X GET "https://flow.icarry.it/api/v1/routing/status" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

PHP

<?php
class iCarryAPIClient {
    private $baseUrl;
    private $username;
    private $password;
    private $token;
    private $tokenExpires;

    public function __construct($baseUrl, $username, $password) {
        $this->baseUrl = $baseUrl;
        $this->username = $username;
        $this->password = $password;
    }

    public function authenticate() {
        $url = $this->baseUrl . "/api/v1/auth/token";
        $data = http_build_query([
            'grant_type' => 'password',
            'username' => $this->username,
            'password' => $this->password
        ]);

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/x-www-form-urlencoded'
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200) {
            throw new Exception("Authentication failed: HTTP $httpCode");
        }

        $tokenData = json_decode($response, true);
        $this->token = $tokenData['access_token'];
        $this->tokenExpires = time() + 86400; // 24 ore

        return $this->token;
    }

    public function getHeaders() {
        if (!$this->token || time() >= $this->tokenExpires) {
            $this->authenticate();
        }

        return [
            'Authorization: Bearer ' . $this->token,
            'Content-Type: application/json'
        ];
    }
}

// Utilizzo
$client = new iCarryAPIClient(
    'https://flow.icarry.it',
    'your_username',
    'your_password'
);
$client->authenticate();
?>

3.4 Best Practices per Sicurezza Token

4. Workflow Completo Step-by-Step

4.1 Panoramica del Flusso

Il workflow completo per integrare l'ottimizzazione routing nel tuo TMS segue questi passaggi:

  1. Preparazione Dati: Estrai driver e consegne dal tuo TMS e convertili nel formato API
  2. Autenticazione: Ottieni token JWT valido
  3. Sottomissione Job: Invia richiesta di ottimizzazione con async_processing=true
  4. Monitoraggio: Polling o WebSocket per tracciare lo stato del job
  5. Recupero Risultati: Quando completato, scarica i risultati
  6. Import nel TMS: Importa le route ottimizzate nel tuo sistema

4.2 Preparazione Dati

I dati devono essere formattati secondo gli schemi DeliveryPointRequest e DriverRequest.

lat/lng nel JSON di POST /optimize restano obbligatori. Se il TMS ha solo l’indirizzo, risolvilo prima con POST /api/v1/geocoding/forward (massimo 10 indirizzi, JWT). POST /api/v1/geocoding/lookup interroga solo la cache e non avvia una nuova risoluzione. Dettaglio nella sezione Geocoding. GET /api/geocode_address e GET /api/reverse_geocode richiedono lo stesso Bearer.

Opzionale (Enterprise): sincronizza i tuoi pin con POST /api/v1/geocoding/pins così i lookup successivi riusano le tue coordinate. Puoi anche attivare la cattura automatica da POST /optimize in /api/v1/geocoding/pins/settings.

Formato Delivery Points

{
  "delivery_points": [
    {
      "tracking_number": "ORD-2025-001",      // Richiesto, ID univoco
      "delivery_company": "Azienda SRL",     // Richiesto
      "address": "Via Roma 1, Milano",        // Richiesto
      "lat": 45.4642,                         // Richiesto, -90 a 90
      "lng": 9.1900,                          // Richiesto, -180 a 180
      "tot_items": 3,                         // Opzionale, default 1
      "priority": 1,                          // Opzionale, 1 = massima … 10 = minima, default 1; a parità di vincoli si scartano prima le priorità basse
      "estimated_duration": 15,              // Opzionale, minuti, default 15
      "time_window_start": "09:00",          // Opzionale, formato HH:MM
      "time_window_end": "12:00",            // Opzionale, formato HH:MM
      "preassigned_driver": "Driver 1",      // Opzionale, forza assegnazione
      "notes": "Consegna urgente",           // Opzionale
      "phone": "+393331234567"               // Opzionale, E.164 (+ e cifre)
    }
  ]
}

Formato Drivers

{
  "drivers": [
    {
      "name": "Driver Milano 01",            // Richiesto
      "start_lat": 45.4642,                   // Richiesto, punto partenza
      "start_lng": 9.1900,                   // Richiesto
      "end_lat": 45.4642,                     // Richiesto, punto ritorno
      "end_lng": 9.1900,                     // Richiesto
      "capacity": 100,                       // Opzionale, default 100
      "vehicle_type": "van_standard",        // Opzionale: tipi legacy o slug veicolo (vedi schema/supporto)
      "routing_profile": null,               // Opzionale: override profilo stradale se esposto dal piano
      "max_distance_km": 250,                // Opzionale, limite distanza
      "hourly_cost": 25.0,                   // Opzionale, default 25.0
      "fuel_consumption": 0.08,              // Opzionale, L/km
      "active": true                         // Opzionale, default true
    }
  ]
}

4.3 Sottomissione Job di Ottimizzazione

⚠️ Importante: Per job di produzione, usa sempre async_processing: true. Il processing sincrono è disponibile solo per test con meno di 20 consegne.

Invia la richiesta completa con tutti i parametri:

POST /api/v1/routing/optimize
Content-Type: application/json
Authorization: Bearer <token>

{
  "async_processing": true,                  // Sempre true per job asincroni
  "session_id": "tms-2025-01-20-001",       // Opzionale, ID univoco sessione
  "webhook_url": "https://your-tms.com/webhook/icarry",  // Opzionale
  "delivery_points": [...],
  "drivers": [...],
  "algorithms": ["standard"],                    // Lista algoritmi da provare
  "optimization_objective": "time",         // time | balanced | distance | cost — vedi #objective-guide
  "optimization_params": {
    "max_iterations": 120,
    "time_limit_seconds": 420,
    "distance_weight": 0.7,
    "time_weight": 0.2,
    "balance_weight": 0.1
  },
  "constraints": {
    "max_route_duration": 480,              // Minuti
    "max_route_distance": 250,              // Km
    "max_stops_per_route": 45,
    "respect_time_windows": true
  }
}

Perché esistono time, distance, balanced e cost: guida alla scelta.

Risposta:

{
  "job_id": "6b758bee-65e5-473e-add8-8cecc36f6fda",
  "status": "queued",
  "estimated_completion": "2025-01-20T10:30:00Z",
  "result_url": "/api/v1/routing/jobs/6b758bee-65e5-473e-add8-8cecc36f6fda"
}

4.4 Monitoraggio Job

Hai due opzioni per monitorare lo stato del job:

Opzione 1: Polling (Raccomandato per TMS)

Esegui polling periodico sull'endpoint di status:

GET /api/v1/routing/jobs/{job_id}
Authorization: Bearer <token>

# Risposta durante processing
{
  "id": "6b758bee-65e5-473e-add8-8cecc36f6fda",
  "status": "processing",
  "progress": 0.65,
  "created_at": "2025-01-20T10:00:00Z",
  "started_at": "2025-01-20T10:00:05Z"
}

# Risposta quando completato
{
  "id": "6b758bee-65e5-473e-add8-8cecc36f6fda",
  "status": "completed",
  "progress": 1.0,
  "result": {
    "routes": [...],
    "global_metrics": {...}
  }
}

Opzione 2: WebSocket (Real-time)

Per aggiornamenti in tempo reale, connettiti via WebSocket:

// JavaScript esempio
const socket = io('wss://flow.icarry.it');
socket.emit('join_room', `job_${jobId}`);

socket.on('job_progress', (data) => {
  console.log(`Progress: ${data.progress * 100}%`);
});

socket.on('job_completed', (data) => {
  console.log('Job completed!', data.result);
});

4.5 Gestione Errori e Retry

Implementa retry logic con exponential backoff:

import time
import random

def poll_job_status(client, job_id, max_attempts=60, initial_delay=5):
    """Polling con retry e exponential backoff"""
    delay = initial_delay
    
    for attempt in range(max_attempts):
        try:
            response = client.get(f"/api/v1/routing/jobs/{job_id}")
            job = response.json()
            
            if job["status"] == "completed":
                return job
            elif job["status"] == "failed":
                raise Exception(f"Job failed: {job.get('error_message')}")
            elif job["status"] == "cancelled":
                raise Exception("Job was cancelled")
            
            # Exponential backoff con jitter
            time.sleep(delay)
            delay = min(delay * 1.5, 30)  # Max 30 secondi
            delay += random.uniform(0, 2)  # Jitter
            
        except requests.exceptions.RequestException as e:
            if attempt < max_attempts - 1:
                time.sleep(delay)
                delay *= 1.5
                continue
            raise
    
    raise TimeoutError("Job did not complete in time")

5. Esempi di Codice Completi

5.1 Esempio Completo Python

import requests
import time
from datetime import datetime
from typing import Dict, List, Optional

class iCarryRoutingClient:
    """Client completo per integrazione API iCarryUP"""
    
    def __init__(self, base_url: str, username: str, password: str):
        self.base_url = base_url.rstrip('/')
        self.username = username
        self.password = password
        self.token = None
        self.token_expires = None
    
    def _authenticate(self):
        """Autenticazione e ottenimento token"""
        url = f"{self.base_url}/api/v1/auth/token"
        data = {
            "grant_type": "password",
            "username": self.username,
            "password": self.password
        }
        response = requests.post(url, data=data)
        response.raise_for_status()
        
        token_data = response.json()
        self.token = token_data["access_token"]
        # Assume 24h validity
        self.token_expires = datetime.now().timestamp() + 86400
        return self.token
    
    def _get_headers(self):
        """Ottiene header con token valido"""
        if not self.token or datetime.now().timestamp() >= self.token_expires:
            self._authenticate()
        return {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
    
    def submit_optimization(
        self,
        delivery_points: List[Dict],
        drivers: List[Dict],
        algorithms: List[str] = ["standard"],
        webhook_url: Optional[str] = None,
        session_id: Optional[str] = None
    ) -> str:
        """Sottomette una richiesta di ottimizzazione"""
        url = f"{self.base_url}/api/v1/routing/optimize"
        payload = {
            "async_processing": True,
            "delivery_points": delivery_points,
            "drivers": drivers,
            "algorithms": algorithms,
            "optimization_objective": "time",
            "optimization_params": {
                "max_iterations": 120,
                "time_limit_seconds": 420,
                "distance_weight": 0.7,
                "time_weight": 0.2,
                "balance_weight": 0.1
            },
            "constraints": {
                "max_route_duration": 480,
                "max_route_distance": 250,
                "respect_time_windows": True
            }
        }
        
        if webhook_url:
            payload["webhook_url"] = webhook_url
        if session_id:
            payload["session_id"] = session_id
        
        response = requests.post(url, json=payload, headers=self._get_headers())
        response.raise_for_status()
        
        result = response.json()
        return result["job_id"]
    
    def get_job_status(self, job_id: str) -> Dict:
        """Ottiene lo status di un job"""
        url = f"{self.base_url}/api/v1/routing/jobs/{job_id}"
        response = requests.get(url, headers=self._get_headers())
        response.raise_for_status()
        return response.json()
    
    def wait_for_completion(
        self,
        job_id: str,
        poll_interval: int = 5,
        max_wait: int = 1800
    ) -> Dict:
        """Attende il completamento di un job con polling"""
        start_time = time.time()
        
        while time.time() - start_time < max_wait:
            job = self.get_job_status(job_id)
            status = job["status"]
            
            if status == "completed":
                return job
            elif status == "failed":
                raise Exception(f"Job failed: {job.get('error_message')}")
            elif status == "cancelled":
                raise Exception("Job was cancelled")
            
            time.sleep(poll_interval)
        
        raise TimeoutError(f"Job {job_id} did not complete in {max_wait}s")
    
    def export_results(self, job_id: str) -> Dict:
        """Esporta risultati in formato completo"""
        url = f"{self.base_url}/api/v1/routing/jobs/{job_id}/export"
        response = requests.get(url, headers=self._get_headers())
        response.raise_for_status()
        return response.json()

# Esempio di utilizzo
if __name__ == "__main__":
    client = iCarryRoutingClient(
        base_url="https://flow.icarry.it",
        username="your_username",
        password="your_password"
    )
    
    # Prepara dati
    delivery_points = [
        {
            "tracking_number": "ORD-001",
            "delivery_company": "Azienda SRL",
            "address": "Via Roma 1, Milano",
            "lat": 45.4642,
            "lng": 9.1900,
            "tot_items": 2,
            "priority": 1
        }
    ]
    
    drivers = [
        {
            "name": "Driver 1",
            "start_lat": 45.4642,
            "start_lng": 9.1900,
            "end_lat": 45.4642,
            "end_lng": 9.1900,
            "capacity": 100,
            "vehicle_type": "van_standard"
        }
    ]
    
    # Sottometti ottimizzazione
    job_id = client.submit_optimization(
        delivery_points=delivery_points,
        drivers=drivers,
        algorithms=["standard"],
        session_id=f"tms-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
    )
    
    print(f"Job submitted: {job_id}")
    
    # Attendi completamento
    completed_job = client.wait_for_completion(job_id)
    
    # Processa risultati
    result = completed_job["result"]
    print(f"Optimization completed!")
    print(f"Total routes: {len(result['routes'])}")
    print(f"Total distance: {result['global_metrics']['total_distance_km']:.2f} km")

5.2 Esempio Completo JavaScript/Node.js

class iCarryRoutingClient {
    constructor(baseUrl, username, password) {
        this.baseUrl = baseUrl.replace(/\/$/, '');
        this.username = username;
        this.password = password;
        this.token = null;
        this.tokenExpires = null;
    }

    async authenticate() {
        const url = `${this.baseUrl}/api/v1/auth/token`;
        const formData = new URLSearchParams();
        formData.append('grant_type', 'password');
        formData.append('username', this.username);
        formData.append('password', this.password);

        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: formData
        });

        if (!response.ok) {
            throw new Error(`Auth failed: ${response.statusText}`);
        }

        const data = await response.json();
        this.token = data.access_token;
        this.tokenExpires = Date.now() + (24 * 60 * 60 * 1000);
        return this.token;
    }

    async getHeaders() {
        if (!this.token || Date.now() >= this.tokenExpires) {
            await this.authenticate();
        }
        return {
            'Authorization': `Bearer ${this.token}`,
            'Content-Type': 'application/json'
        };
    }

    async submitOptimization(deliveryPoints, drivers, options = {}) {
        const url = `${this.baseUrl}/api/v1/routing/optimize`;
        const payload = {
            async_processing: true,
            delivery_points: deliveryPoints,
            drivers: drivers,
            algorithms: options.algorithms || ['standard'],
            optimization_objective: options.optimization_objective || 'time',
            optimization_params: {
                max_iterations: 120,
                time_limit_seconds: 420,
                distance_weight: 0.7,
                time_weight: 0.2,
                balance_weight: 0.1
            },
            constraints: {
                max_route_duration: 480,
                max_route_distance: 250,
                respect_time_windows: true
            },
            ...options
        };

        const response = await fetch(url, {
            method: 'POST',
            headers: await this.getHeaders(),
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            throw new Error(`Submit failed: ${response.statusText}`);
        }

        const result = await response.json();
        return result.job_id;
    }

    async getJobStatus(jobId) {
        const url = `${this.baseUrl}/api/v1/routing/jobs/${jobId}`;
        const response = await fetch(url, {
            headers: await this.getHeaders()
        });

        if (!response.ok) {
            throw new Error(`Status check failed: ${response.statusText}`);
        }

        return await response.json();
    }

    async waitForCompletion(jobId, pollInterval = 5000, maxWait = 1800000) {
        const startTime = Date.now();

        while (Date.now() - startTime < maxWait) {
            const job = await this.getJobStatus(jobId);
            const status = job.status;

            if (status === 'completed') {
                return job;
            } else if (status === 'failed') {
                throw new Error(`Job failed: ${job.error_message}`);
            } else if (status === 'cancelled') {
                throw new Error('Job was cancelled');
            }

            await new Promise(resolve => setTimeout(resolve, pollInterval));
        }

        throw new Error(`Job ${jobId} did not complete in time`);
    }

    async exportResults(jobId) {
        const url = `${this.baseUrl}/api/v1/routing/jobs/${jobId}/export`;
        const response = await fetch(url, {
            headers: await this.getHeaders()
        });

        if (!response.ok) {
            throw new Error(`Export failed: ${response.statusText}`);
        }

        return await response.json();
    }
}

// Esempio di utilizzo
(async () => {
    const client = new iCarryRoutingClient(
        'https://flow.icarry.it',
        'your_username',
        'your_password'
    );

    const deliveryPoints = [{
        tracking_number: 'ORD-001',
        delivery_company: 'Azienda SRL',
        address: 'Via Roma 1, Milano',
        lat: 45.4642,
        lng: 9.1900,
        tot_items: 2,
        priority: 1
    }];

    const drivers = [{
        name: 'Driver 1',
        start_lat: 45.4642,
        start_lng: 9.1900,
        end_lat: 45.4642,
        end_lng: 9.1900,
        capacity: 100,
        vehicle_type: 'van_standard'
    }];

    try {
        const jobId = await client.submitOptimization(deliveryPoints, drivers);
        console.log(`Job submitted: ${jobId}`);

        const completedJob = await client.waitForCompletion(jobId);
        const result = completedJob.result;
        
        console.log('Optimization completed!');
        console.log(`Total routes: ${result.routes.length}`);
        console.log(`Total distance: ${result.global_metrics.total_distance_km.toFixed(2)} km`);
    } catch (error) {
        console.error('Error:', error);
    }
})();

5.3 Esempio cURL Completo

#!/bin/bash

# Configurazione
BASE_URL="https://flow.icarry.it"
USERNAME="your_username"
PASSWORD="your_password"

# 1. Autenticazione
echo "Authenticating..."
TOKEN_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/v1/auth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=${USERNAME}&password=${PASSWORD}")

TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.access_token')
echo "Token obtained: ${TOKEN:0:20}..."

# 2. Sottomissione ottimizzazione
echo "Submitting optimization..."
JOB_RESPONSE=$(curl -s -X POST "${BASE_URL}/api/v1/routing/optimize" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "async_processing": true,
    "delivery_points": [
      {
        "tracking_number": "ORD-001",
        "delivery_company": "Azienda SRL",
        "address": "Via Roma 1, Milano",
        "lat": 45.4642,
        "lng": 9.1900,
        "tot_items": 2,
        "priority": 1
      }
    ],
    "drivers": [
      {
        "name": "Driver 1",
        "start_lat": 45.4642,
        "start_lng": 9.1900,
        "end_lat": 45.4642,
        "end_lng": 9.1900,
        "capacity": 100,
        "vehicle_type": "van_standard"
      }
    ],
    "algorithms": ["standard"],
    "optimization_objective": "time"
  }')

JOB_ID=$(echo $JOB_RESPONSE | jq -r '.job_id')
echo "Job ID: ${JOB_ID}"

# 3. Polling status
echo "Waiting for completion..."
while true; do
  STATUS_RESPONSE=$(curl -s -X GET "${BASE_URL}/api/v1/routing/jobs/${JOB_ID}" \
    -H "Authorization: Bearer ${TOKEN}")
  
  STATUS=$(echo $STATUS_RESPONSE | jq -r '.status')
  PROGRESS=$(echo $STATUS_RESPONSE | jq -r '.progress')
  
  echo "Status: ${STATUS} (${PROGRESS})"
  
  if [ "$STATUS" = "completed" ]; then
    echo "Job completed!"
    echo $STATUS_RESPONSE | jq '.result.global_metrics'
    break
  elif [ "$STATUS" = "failed" ]; then
    echo "Job failed!"
    echo $STATUS_RESPONSE | jq '.error_message'
    exit 1
  fi
  
  sleep 5
done

6. Gestione Webhook

6.1 Cos'è un Webhook?

Un webhook è un callback HTTP che iCarryUP invia al tuo server quando un job di ottimizzazione viene completato. Questo elimina la necessità di polling continuo.

6.2 Configurazione Webhook

Per abilitare i webhook, includi webhook_url nella richiesta di ottimizzazione:

{
  "async_processing": true,
  "webhook_url": "https://your-tms.com/api/webhooks/icarry-routing",
  "delivery_points": [...],
  "drivers": [...]
}

Requisiti webhook URL:

6.3 Payload Webhook

Il webhook invia una POST con il seguente payload (identico a RoutingResultResponse):

POST https://your-tms.com/api/webhooks/icarry-routing
Content-Type: application/json


{
  "request_id": "6b758bee-65e5-473e-add8-8cecc36f6fda",
  "status": "completed",
  "routes": [
    {
      "driver_id": "driver_1",
      "driver_name": "Driver Milano 01",
      "delivery_points": [...],
      "metrics": {
        "total_distance_km": 125.5,
        "total_time_minutes": 240,
        "total_cost": 150.00,
        "num_stops": 15
      },
      "route_geometries": [...]
    }
  ],
  "global_metrics": {
    "total_distance_km": 450.2,
    "total_time_minutes": 720,
    "total_cost": 550.00,
    "route_balance_score": 0.85
  },
  "algorithm_used": "standard",
  "execution_time": 45.3,
  "created_at": "2025-01-20T10:00:00Z"
}

6.4 Esempio Server Webhook (Python Flask)

Per esempi di implementazione del server webhook e verifica della firma (X-iCarry-Signature), contattare il supporto tecnico.

6.5 Retry Logic

iCarryUP implementa retry automatici se il webhook fallisce:

Il webhook viene considerato consegnato se restituisce status code < 300. Per evitare retry infiniti, restituisci sempre 200 anche in caso di errori di processing (gestiscili internamente).

6.6 Best Practices Webhook

7. Integrazione con TMS Popolari

7.1 Pattern Generale di Integrazione

La maggior parte dei TMS segue questo pattern:

  1. Estrazione dati: Query al database TMS per ottenere consegne e driver
  2. Trasformazione: Conversione dati TMS → formato API iCarryUP
  3. Ottimizzazione: Chiamata API iCarryUP
  4. Import risultati: Conversione risultati API → formato TMS e salvataggio

7.2 Esempio: Integrazione con Database SQL

import psycopg2
from iCarryRoutingClient import iCarryRoutingClient

def sync_tms_to_icarry():
    """Sincronizza dati TMS con iCarryUP e ottimizza"""
    
    # Connessione database TMS
    conn = psycopg2.connect(
        host="tms-db.example.com",
        database="tms_db",
        user="tms_user",
        password="tms_password"
    )
    cursor = conn.cursor()
    
    # Estrai consegne pendenti
    cursor.execute("""
        SELECT order_id, address, lat, lng, items_count, priority, time_window_start, time_window_end
        FROM deliveries
        WHERE status = 'pending' AND delivery_date = CURRENT_DATE
    """)
    
    delivery_points = []
    for row in cursor.fetchall():
        delivery_points.append({
            "tracking_number": f"ORD-{row[0]}",
            "delivery_company": "Cliente",
            "address": row[1],
            "lat": float(row[2]),
            "lng": float(row[3]),
            "tot_items": row[4],
            "priority": row[5] or 1,
            "time_window_start": row[6].strftime("%H:%M") if row[6] else None,
            "time_window_end": row[7].strftime("%H:%M") if row[7] else None
        })
    
    # Estrai driver attivi
    cursor.execute("""
        SELECT driver_id, name, depot_lat, depot_lng, vehicle_type, max_capacity
        FROM drivers
        WHERE active = true AND available_date = CURRENT_DATE
    """)
    
    drivers = []
    for row in cursor.fetchall():
        drivers.append({
            "name": row[1],
            "start_lat": float(row[2]),
            "start_lng": float(row[3]),
            "end_lat": float(row[2]),  # Stesso punto di partenza
            "end_lng": float(row[3]),
            "vehicle_type": row[4] or "van_standard",
            "capacity": row[5] or 100
        })
    
    cursor.close()
    conn.close()
    
    # Ottimizza con iCarryUP
    client = iCarryRoutingClient(
        base_url="https://flow.icarry.it",
        username="tms_user",
        password="tms_password"
    )
    
    job_id = client.submit_optimization(
        delivery_points=delivery_points,
        drivers=drivers,
        session_id=f"tms-sync-{datetime.now().strftime('%Y%m%d')}"
    )
    
    # Attendi completamento
    completed_job = client.wait_for_completion(job_id)
    result = completed_job["result"]
    
    # Importa risultati nel TMS
    import_routes_to_tms(result["routes"])
    
    return job_id

def import_routes_to_tms(routes):
    """Importa route ottimizzate nel database TMS"""
    conn = psycopg2.connect(...)
    cursor = conn.cursor()
    
    for route in routes:
        driver_name = route["driver_name"]
        delivery_points = route["delivery_points"]
        
        # Trova driver_id
        cursor.execute("SELECT driver_id FROM drivers WHERE name = %s", (driver_name,))
        driver_id = cursor.fetchone()[0]
        
        # Salva route
        cursor.execute("""
            INSERT INTO routes (driver_id, route_date, total_distance, total_time)
            VALUES (%s, CURRENT_DATE, %s, %s)
            RETURNING route_id
        """, (driver_id, route["metrics"]["total_distance_km"], route["metrics"]["total_time_minutes"]))
        
        route_id = cursor.fetchone()[0]
        
        # Salva stops nella sequenza ottimizzata
        for order, point in enumerate(delivery_points, start=1):
            tracking_number = point["tracking_number"]
            order_id = int(tracking_number.replace("ORD-", ""))
            
            cursor.execute("""
                INSERT INTO route_stops (route_id, order_id, stop_sequence)
                VALUES (%s, %s, %s)
            """, (route_id, order_id, order))
            
            # Aggiorna status consegna
            cursor.execute("""
                UPDATE deliveries
                SET status = 'assigned', route_id = %s, stop_sequence = %s
                WHERE order_id = %s
            """, (route_id, order, order_id))
    
    conn.commit()
    cursor.close()
    conn.close()

7.3 Esempio: Integrazione con API REST TMS

import requests

class TMSIntegration:
    """Integrazione tra TMS REST API e iCarryUP"""
    
    def __init__(self, tms_api_url, tms_token, icarry_client):
        self.tms_api_url = tms_api_url
        self.tms_token = tms_token
        self.icarry_client = icarry_client
    
    def get_tms_headers(self):
        return {
            "Authorization": f"Bearer {self.tms_token}",
            "Content-Type": "application/json"
        }
    
    def fetch_pending_deliveries(self):
        """Recupera consegne pendenti dal TMS"""
        response = requests.get(
            f"{self.tms_api_url}/api/deliveries/pending",
            headers=self.get_tms_headers()
        )
        response.raise_for_status()
        return response.json()
    
    def fetch_available_drivers(self):
        """Recupera driver disponibili dal TMS"""
        response = requests.get(
            f"{self.tms_api_url}/api/drivers/available",
            headers=self.get_tms_headers()
        )
        response.raise_for_status()
        return response.json()
    
    def optimize_and_sync(self):
        """Ottimizza e sincronizza risultati con TMS"""
        # 1. Recupera dati da TMS
        tms_deliveries = self.fetch_pending_deliveries()
        tms_drivers = self.fetch_available_drivers()
        
        # 2. Converti formato
        delivery_points = [
            {
                "tracking_number": d["order_number"],
                "delivery_company": d["customer_name"],
                "address": d["delivery_address"],
                "lat": d["latitude"],
                "lng": d["longitude"],
                "tot_items": d["items_count"],
                "priority": d.get("priority", 1)
            }
            for d in tms_deliveries
        ]
        
        
        # vehicle_type: valori legacy o slug supportati (vedi schema / supporto)
        
        drivers = [
            {
                "name": d["driver_name"],
                "start_lat": d["depot"]["latitude"],
                "start_lng": d["depot"]["longitude"],
                "end_lat": d["depot"]["latitude"],
                "end_lng": d["depot"]["longitude"],
                "capacity": d["vehicle"]["capacity"],
                "vehicle_type": d["vehicle"]["type"]
            }
            for d in tms_drivers
        ]
        
        # 3. Ottimizza con iCarryUP
        job_id = self.icarry_client.submit_optimization(
            delivery_points=delivery_points,
            drivers=drivers
        )
        
        # 4. Attendi completamento
        completed_job = self.icarry_client.wait_for_completion(job_id)
        routes = completed_job["result"]["routes"]
        
        # 5. Importa risultati nel TMS
        self.import_routes_to_tms(routes)
        
        return job_id
    
    def import_routes_to_tms(self, routes):
        """Importa route nel TMS via API"""
        for route in routes:
            # Crea route nel TMS
            route_data = {
                "driver_name": route["driver_name"],
                "stops": [
                    {
                        "order_number": point["tracking_number"].replace("ORD-", ""),
                        "sequence": idx + 1
                    }
                    for idx, point in enumerate(route["delivery_points"])
                ],
                "metrics": {
                    "total_distance_km": route["metrics"]["total_distance_km"],
                    "total_time_minutes": route["metrics"]["total_time_minutes"]
                }
            }
            
            response = requests.post(
                f"{self.tms_api_url}/api/routes",
                json=route_data,
                headers=self.get_tms_headers()
            )
            response.raise_for_status()

7.4 Schedulazione Automatica

Per automatizzare l'ottimizzazione, usa un job scheduler (cron, Celery, etc.):

# Esempio con cron (Linux)
# Esegui ottimizzazione ogni mattina alle 6:00
0 6 * * * /usr/bin/python3 /path/to/tms_sync.py

# Esempio con Python schedule
import schedule
import time

def daily_optimization():
    """Esegue ottimizzazione giornaliera"""
    integration = TMSIntegration(...)
    integration.optimize_and_sync()

# Schedula ogni giorno alle 6:00
schedule.every().day.at("06:00").do(daily_optimization)

while True:
    schedule.run_pending()
    time.sleep(60)

8. Gestione Risultati e Export

8.1 Struttura Risultati

I risultati completi sono disponibili nel campo result della risposta job:

{
  "result": {
    "request_id": "job-uuid",
    "status": "completed",
    "routes": [
      {
        "driver_id": "driver_1",
        "driver_name": "Driver Milano 01",
        "delivery_points": [
          {
            "id": "point_1",
            "tracking_number": "ORD-001",
            "address": "Via Roma 1, Milano",
            "lat": 45.4642,
            "lng": 9.1900,
            "order_in_route": 1,  // Sequenza nella route
            "tot_items": 2,
            "priority": 1
          }
        ],
        "metrics": {
          "total_distance_km": 125.5,
          "total_time_minutes": 240,
          "travel_time_minutes": 180,
          "service_time_minutes": 60,
          "total_cost": 150.00,
          "fuel_cost": 20.00,
          "driver_cost": 100.00,
          "toll_cost": 30.00,
          "num_stops": 15,
          "efficiency_score": 0.85
        },
        "route_geometries": [
          {
            "type": "LineString",
            "coordinates": [[9.1900, 45.4642], [9.2000, 45.4700], ...]
          }
        ]
      }
    ],
    "global_metrics": {
      "total_distance_km": 450.2,
      "total_time_minutes": 720,
      "travel_time_minutes": 540,
      "service_time_minutes": 180,
      "total_cost": 550.00,
      "average_route_distance": 112.55,
      "route_balance_score": 0.85,
      "algorithm_execution_time": 45.3,
      "optimization_score": 0.92
    },
    "algorithm_used": "standard",
    "execution_time": 45.3,
    "created_at": "2025-01-20T10:00:00Z"
  }
}

8.2 Parsing Risultati

def process_optimization_results(result):
    """Processa e analizza risultati ottimizzazione"""
    
    routes = result["routes"]
    global_metrics = result["global_metrics"]
    
    print(f"Ottimizzazione completata!")
    print(f"Algoritmo: {result['algorithm_used']}")
    print(f"Tempo esecuzione: {result['execution_time']:.2f}s")
    print(f"\nMetriche Globali:")
    print(f"  Distanza totale: {global_metrics['total_distance_km']:.2f} km")
    print(f"  Tempo totale: {global_metrics['total_time_minutes']:.0f} minuti")
    print(f"  Costo totale: €{global_metrics['total_cost']:.2f}")
    print(f"  Bilanciamento route: {global_metrics['route_balance_score']:.2f}")
    
    print(f"\nDettaglio Route ({len(routes)} driver):")
    for route in routes:
        print(f"\n  {route['driver_name']}:")
        print(f"    Tappe: {route['metrics']['num_stops']}")
        print(f"    Distanza: {route['metrics']['total_distance_km']:.2f} km")
        print(f"    Durata: {route['metrics']['total_time_minutes']:.0f} min")
        print(f"    Costo: €{route['metrics']['total_cost']:.2f}")
        
        # Mostra sequenza consegne
        print(f"    Sequenza:")
        for point in route['delivery_points']:
            print(f"      {point['order_in_route']}. {point['address'][:40]}")

8.3 Export CSV/Excel

Usa l'endpoint dedicato per export:

GET /api/v1/routing/jobs/{job_id}/export
Authorization: Bearer <token>

# Restituisce risultati in formato JSON completo
# Per CSV/Excel, processa i risultati lato client:

import csv
import json

def export_routes_to_csv(routes, filename):
    """Esporta route in formato CSV"""
    with open(filename, 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow([
            'Driver', 'Stop', 'Tracking Number', 'Address', 
            'Lat', 'Lng', 'Items', 'Priority'
        ])
        
        for route in routes:
            driver_name = route['driver_name']
            for point in route['delivery_points']:
                writer.writerow([
                    driver_name,
                    point['order_in_route'],
                    point['tracking_number'],
                    point['address'],
                    point['lat'],
                    point['lng'],
                    point['tot_items'],
                    point['priority']
                ])

8.4 Generazione Mappe

Per generare mappe HTML per i driver:

# Genera mappa completa
POST /api/v1/routing/jobs/{job_id}/generate-real-map
Authorization: Bearer <token>

# Download mappa
GET /api/v1/routing/jobs/{job_id}/download-map
Authorization: Bearer <token>

# Download mappa singolo driver
GET /api/v1/routing/jobs/{job_id}/download-driver-app?driver_name={driver_name}
Authorization: Bearer <token>

8.5 Import Risultati nel TMS

def import_results_to_tms_database(routes, connection):
    """Importa risultati nel database TMS"""
    cursor = connection.cursor()
    
    for route in routes:
        driver_name = route['driver_name']
        metrics = route['metrics']
        
        # Crea route record
        cursor.execute("""
            INSERT INTO optimized_routes 
            (driver_name, total_distance, total_time, total_cost, created_at)
            VALUES (%s, %s, %s, %s, NOW())
            RETURNING route_id
        """, (
            driver_name,
            metrics['total_distance_km'],
            metrics['total_time_minutes'],
            metrics['total_cost']
        ))
        
        route_id = cursor.fetchone()[0]
        
        # Inserisci stops nella sequenza ottimizzata
        for point in route['delivery_points']:
            cursor.execute("""
                INSERT INTO route_stops 
                (route_id, tracking_number, stop_sequence, address, lat, lng)
                VALUES (%s, %s, %s, %s, %s, %s)
            """, (
                route_id,
                point['tracking_number'],
                point['order_in_route'],
                point['address'],
                point['lat'],
                point['lng']
            ))
    
    connection.commit()

9. Best Practices e Ottimizzazioni

9.1 Dimensionamento Batch

Batch size consigliati:

Per volumi elevati, suddividi in più job paralleli:

# Esempio: Suddivisione automatica
def optimize_large_batch(delivery_points, drivers, batch_size=100):
    """Ottimizza batch grandi suddividendoli"""
    batches = [
        delivery_points[i:i+batch_size] 
        for i in range(0, len(delivery_points), batch_size)
    ]
    
    job_ids = []
    for batch in batches:
        job_id = client.submit_optimization(
            delivery_points=batch,
            drivers=drivers,
            session_id=f"batch-{len(job_ids)+1}"
        )
        job_ids.append(job_id)
    
    # Attendi tutti i job
    results = []
    for job_id in job_ids:
        job = client.wait_for_completion(job_id)
        results.append(job["result"])
    
    # Unisci risultati se necessario
    return merge_results(results)

9.2 Rate Limiting

L'API implementa rate limiting per proteggere il sistema:

Se ricevi 429 Too Many Requests, implementa backoff:

import time
from requests.exceptions import HTTPError

def request_with_retry(client, max_retries=3):
    """Richiesta con retry e rate limiting"""
    for attempt in range(max_retries):
        try:
            return client.make_request()
        except HTTPError as e:
            if e.response.status_code == 429:
                retry_after = int(e.response.headers.get('Retry-After', 60))
                if attempt < max_retries - 1:
                    time.sleep(retry_after)
                    continue
            raise

9.3 Caching Strategico

Cache i seguenti dati per ridurre chiamate API:

9.4 Gestione Job Paralleli

Per gestire multiple ottimizzazioni simultanee:

import asyncio
import aiohttp

async def optimize_multiple_batches_async(batches, drivers):
    """Ottimizza multiple batch in parallelo"""
    async with aiohttp.ClientSession() as session:
        # Sottometti tutti i job
        tasks = []
        for batch in batches:
            task = submit_optimization_async(session, batch, drivers)
            tasks.append(task)
        
        job_ids = await asyncio.gather(*tasks)
        
        # Monitora tutti i job
        monitor_tasks = [
            monitor_job_async(session, job_id) 
            for job_id in job_ids
        ]
        
        results = await asyncio.gather(*monitor_tasks)
        return results

9.5 Error Handling Robusto

def robust_optimization_request(client, delivery_points, drivers, max_retries=3):
    """Richiesta ottimizzazione con error handling completo"""
    for attempt in range(max_retries):
        try:
            job_id = client.submit_optimization(
                delivery_points=delivery_points,
                drivers=drivers
            )
            return job_id
            
        except requests.exceptions.ConnectionError:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
                continue
            raise Exception("Cannot connect to API after retries")
            
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 401:
                # Token scaduto, rigenera
                client._authenticate()
                continue
            elif e.response.status_code == 422:
                # Validation error, non retry
                raise Exception(f"Validation error: {e.response.json()}")
            elif e.response.status_code >= 500:
                # Server error, retry
                if attempt < max_retries - 1:
                    time.sleep(2 ** attempt)
                    continue
            raise
            
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)
                continue
            raise

9.6 Monitoring e Logging

Implementa logging completo per debugging e monitoring:

import logging

logger = logging.getLogger('tms_integration')
logger.setLevel(logging.INFO)

def log_optimization_attempt(delivery_count, driver_count, job_id):
    """Log tentativo ottimizzazione"""
    logger.info(f"Optimization submitted", extra={
        'delivery_count': delivery_count,
        'driver_count': driver_count,
        'job_id': job_id
    })

def log_optimization_result(job_id, success, metrics=None, error=None):
    """Log risultato ottimizzazione"""
    if success:
        logger.info(f"Optimization completed", extra={
            'job_id': job_id,
            'total_distance': metrics.get('total_distance_km'),
            'total_cost': metrics.get('total_cost')
        })
    else:
        logger.error(f"Optimization failed", extra={
            'job_id': job_id,
            'error': error
        })

9.7 Performance Tips

10. Troubleshooting

10.1 Errori Comuni e Soluzioni

Errore Causa Soluzione
401 Unauthorized Token mancante o scaduto Rigenera token con authenticate()
422 Validation Error Dati non validi (coordinate fuori range, campi mancanti) Verifica formato dati, controlla detail nella risposta
403 Forbidden Algoritmo bloccato per l'utente Usa algoritmo diverso o richiedi sblocco
409 Conflict Job duplicato (stesso session_id) Usa session_id univoco per ogni job
500 Internal Server Error Errore server o servizio routing non disponibile Retry con backoff, verifica status API
504 Gateway Timeout Job troppo grande o timeout processing Riduci batch size o aumenta time_limit_seconds

10.2 Debug Logging

Abilita logging dettagliato per debugging:

import logging
import http.client

# Abilita debug HTTP requests
logging.basicConfig(level=logging.DEBUG)
http.client.HTTPConnection.debuglevel = 1

# Log tutte le richieste/resposte
import requests
import json

class LoggingSession(requests.Session):
    def request(self, *args, **kwargs):
        logger.debug(f"Request: {args[0]} {args[1]}")
        logger.debug(f"Headers: {kwargs.get('headers', {})}")
        if 'json' in kwargs:
            logger.debug(f"Payload: {json.dumps(kwargs['json'], indent=2)}")
        
        response = super().request(*args, **kwargs)
        
        logger.debug(f"Response: {response.status_code}")
        logger.debug(f"Response body: {response.text[:500]}")
        
        return response

# Usa LoggingSession invece di requests normale
client.session = LoggingSession()

10.3 Verifica Connettività

def verify_api_connectivity(base_url):
    """Verifica che l'API sia raggiungibile"""
    try:
        response = requests.get(
            f"{base_url}/api/v1/routing/status",
            timeout=10
        )
        if response.status_code == 200:
            status = response.json()
            print(f"✅ API raggiungibile")
            print(f"   Versione: {status.get('version')}")
            print(f"   Status: {status.get('status')}")
            print(f"   Job attivi: {status.get('active_jobs')}")
            return True
        else:
            print(f"❌ API risponde con status {response.status_code}")
            return False
    except requests.exceptions.ConnectionError:
        print("❌ Impossibile connettersi all'API")
        return False
    except requests.exceptions.Timeout:
        print("❌ Timeout connessione API")
        return False

10.4 Validazione Dati Pre-Invio

def validate_delivery_point(point):
    """Valida un delivery point prima dell'invio"""
    errors = []
    
    # Coordinate valide
    if not (-90 <= point.get('lat', 0) <= 90):
        errors.append("lat deve essere tra -90 e 90")
    if not (-180 <= point.get('lng', 0) <= 180):
        errors.append("lng deve essere tra -180 e 180")
    
    # Campi richiesti
    required = ['tracking_number', 'delivery_company', 'address', 'lat', 'lng']
    for field in required:
        if not point.get(field):
            errors.append(f"{field} è richiesto")
    
    # Time window valido
    if point.get('time_window_start') and point.get('time_window_end'):
        start = point['time_window_start']
        end = point['time_window_end']
        if end <= start:
            errors.append("time_window_end deve essere dopo time_window_start")
    
    return errors

def validate_before_submit(delivery_points, drivers):
    """Valida tutti i dati prima di sottomettere"""
    all_errors = []
    
    for idx, point in enumerate(delivery_points):
        errors = validate_delivery_point(point)
        if errors:
            all_errors.append(f"Delivery point {idx}: {', '.join(errors)}")
    
    if len(delivery_points) == 0:
        all_errors.append("Almeno un delivery point è richiesto")
    if len(drivers) == 0:
        all_errors.append("Almeno un driver è richiesto")
    
    if all_errors:
        raise ValueError("Validation errors:\n" + "\n".join(all_errors))

10.5 Metriche e Monitoring

Monitora le seguenti metriche per ottimizzare l'integrazione:

10.5b Novità API (allineamento console)

Endpoint aggiuntivi rispetto al flusso base optimize → poll job (Bearer obbligatorio salvo diversa indicazione):

10.6 Supporto e Contatti

Per assistenza tecnica o domande sull'integrazione:

Quando contatti il supporto, includi: