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
- Carica i dati logistici (driver + consegne) nel formato previsto da
RoutingOptimizationRequest. - Invia POST /api/v1/routing/optimize con async_processing=true; ricevi un job_id.
- Monitora lo stato via GET /api/v1/routing/jobs/{job_id} (polling 5-10s) o via WebSocket /ws/jobs.
- Quando status=completed leggi result, esporta file o genera mappe con gli endpoint dedicati.
- 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_number | tracking_number | sì | stringa ≤64 | ID tappa |
delivery_address | address | sì (o lat+lng) | stringa | Geocoding / display |
delivery_company | delivery_company | sì in JSON (default import «Cliente») | stringa | Metadato |
lat / lng | lat / lng | sì in JSON; opz. in Excel se c’è indirizzo | gradi | Matrice / geometria |
tot_items | tot_items | no (default 1) | int ≥1 | Capacità (colli) |
priority | priority | no (default 1) | 1–10 (1 = alta) | Priorità / drop |
estimated_duration | estimated_duration | no | minuti | Tempo di sosta |
notes | notes | no | stringa ≤500 | Solo app driver |
phone | phone | no | E.164 (+ e cifre, es. +393331234567) | Telefono destinatario (link tel in app e Control Tower) |
preassigned_driver | preassigned_driver | no | drivers[].name | Lock → mandatory_assignments |
point_type | point_type | no (default delivery) | pickup | delivery | Ordine ritiro→consegna; pickup senza collegamento = ritiro autonomo |
pickup_point_id | pickup_point_id | no | stringa | Collegamento ritiro→consegna; assente = ritiro autonomo (rientro in base) |
time_window_start / end | stesso | no | HH:MM | Finestre orarie |
volume_m3 / weight_kg | stesso | no | float ≥0 | Cap 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 |
|---|---|---|---|
name | name | sì | Identità |
phone | phone | no | Telefono fattorino opzionale in E.164 |
start_address / end_address | — | start obbligatorio in import | Geocoding → start_* / end_* |
lat / lng | start_lat / start_lng | sì in JSON | Deposito partenza |
end_lat / end_lng | end_lat / end_lng | sì in JSON (default = start) | Rientro |
capacity | capacity | no (default 50) | Max colli (tot_items) |
max_volume_m3 | max_volume_m3 | no | Cap volume |
vehicle_type | vehicle_type | no (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_profile | routing_profile | no | Override: car, van_standard, van_maxi, truck_7_5t, truck_12t, truck_18t, truck_32t, artic_44t, icarry_cargo_bike |
max_distance_km | max_distance_km | no | Cap km |
hourly_cost, fuel_consumption, prezzo_litro, costo_pedaggio_km | stesso | no | Costi / report |
start_time / end_time | stesso | no (08:00–18:00 se coppia) | Turno |
break_start / break_end | stesso | in coppia | Pausa |
max_minutes | max_minutes | no | Durata 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 |
|---|---|---|
car | Auto (M1) | Auto standard. |
van Alias | Furgoni (N1) | Stesso effetto di van_standard. |
van_standard | Furgoni (N1) | Furgone compatto fino a 2.5 t, altezza circa 1.9 m. |
van_maxi | Furgoni (N1) | Furgone tetto alto fino a 3.5 t, altezza circa 2.7 m. |
truck Alias | Autocarri | Stesso effetto di truck_7_5t. |
truck_7_5t | Autocarri | Camion leggero urbano, MMA 7.5 t. |
truck_12t | Autocarri | Camion medio, MMA 12 t. Esempio: Iveco TGM 13.250 4x4 → truck_12t. |
truck_18t | Autocarri | Camion pesante 2 assi, MMA 18 t. |
truck_32t | Autocarri | Camion 4 assi, MMA 32 t. |
artic_44t | Autoarticolati | Bilico 44 t, circa 16.5 m. |
bike Alias | Ciclo-logistica | Stesso effetto di icarry_cargo_bike. |
scooter Alias | Auto (M1) | Stesso effetto di car. |
icarry_cargo_bike | Ciclo-logistica | Cargo 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:
- Consegne: address/delivery_address ← indirizzo, destinazione, location · delivery_company ← destinatario, cliente, company · tot_items ← colli, quantity, pezzi · phone ← telefono, tel, cellulare, mobile · time_window_start/end ← fascia_dalle/alle, dalle/alle, window_start/end.
- Driver: name ← nome, autista, fattorino · start_address/end_address ← partenza/rientro, departure/arrival · capacity ← capacita, max_consegne · start_time/end_time ← turno_inizio/fine, shift_start/end.
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
- Template CSV/XLSX — GET /api/v1/templates/driver-template e /api/v1/templates/delivery-template. Dizionario colonne: vedi «Dizionario dati di input» in questa pagina.
- Mappe driver — GET /api/v1/routing/driver-maps elenca le mappe del tenant (JWT). Gli URL HTML della singola mappa restano capability link.
- Driver App HTML — GET /api/v1/routing/jobs/{job_id}/download-driver-app?driver_name={driver_name} restituisce la versione offline della mappa singolo driver.
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?
- Automazione completa: Integra l'ottimizzazione routing direttamente nel tuo workflow TMS
- Scalabilità: Gestisci centinaia di ottimizzazioni al giorno senza intervento manuale
- Real-time: Ricevi risultati ottimizzati e importali automaticamente nel tuo sistema
- Flessibilità: Personalizza il flusso di lavoro secondo le tue esigenze
- Affidabilità: Sistema robusto con retry automatici, webhook e monitoraggio
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
- Ottimizzazione giornaliera: Pianifica automaticamente le route ogni mattina
- Re-ottimizzazione dinamica: Ricalcola route quando arrivano nuove consegne
- Integrazione con WMS: Collega il sistema di magazzino per ottimizzazioni end-to-end
- Multi-tenant: Gestisci ottimizzazioni per più clienti/aziende
- Analisi e reporting: Raccogli metriche per dashboard e KPI
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:
- Creazione account tecnico dedicato
- Configurazione ruoli e permessi
- Whitelist IP per sicurezza aggiuntiva
- Accesso a funzionalità avanzate (se necessario)
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
}
/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.
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 esserepasswordusername: Il tuo usernamepassword: 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
- Non esporre token in log: Mai loggare token completi
- Rinnova proattivamente: Rinnova il token prima della scadenza (es. 1 ora prima)
- Gestisci errori 401: Se ricevi 401, rigenera il token e riprova
- Usa HTTPS sempre: Mai inviare token su connessioni non cifrate
- Storage sicuro: Memorizza token in variabili d'ambiente o vault segreti
- Token per servizio: Usa account tecnici dedicati per integrazioni server-to-server
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:
- Preparazione Dati: Estrai driver e consegne dal tuo TMS e convertili nel formato API
- Autenticazione: Ottieni token JWT valido
- Sottomissione Job: Invia richiesta di ottimizzazione con
async_processing=true - Monitoraggio: Polling o WebSocket per tracciare lo stato del job
- Recupero Risultati: Quando completato, scarica i risultati
- 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
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:
- Deve essere HTTPS in produzione
- Deve essere raggiungibile pubblicamente
- Deve rispondere con status code < 300 per confermare ricezione
- Timeout massimo: 30 secondi
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:
- Retry 1: Dopo 15 secondi
- Retry 2: Dopo 60 secondi
- Retry 3: Dopo 180 secondi
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
- Idempotenza: Gestisci webhook duplicati (usa
request_idper deduplicazione) - Processing asincrono: Processa webhook in background per rispondere rapidamente
- Logging: Logga tutti i webhook ricevuti per debugging
- Validazione: Valida sempre il payload prima di processarlo
- Timeout: Rispondi entro 10 secondi per evitare timeout
7. Integrazione con TMS Popolari
7.1 Pattern Generale di Integrazione
La maggior parte dei TMS segue questo pattern:
- Estrazione dati: Query al database TMS per ottenere consegne e driver
- Trasformazione: Conversione dati TMS → formato API iCarryUP
- Ottimizzazione: Chiamata API iCarryUP
- 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:
- 80-120 consegne: Dimensione ottimale per la maggior parte dei casi
- 150+ consegne: Considera frazionamento automatico con
allow_partial_assignments: true - 50-80 consegne: Ideale per ottimizzazioni rapide (< 2 minuti)
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:
- Default: 100 richieste/minuto per utente
- Burst: Fino a 20 richieste in 10 secondi
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:
- Token JWT: Cache fino a scadenza (24h)
- Algoritmi disponibili: Cache per 1 ora
- Status job: Non cache, sempre fresh
- Risultati job: Cache dopo completamento
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
- Usa webhook invece di polling: Riduce carico server e latenza
- Batch multiple richieste: Raggruppa operazioni quando possibile
- Connection pooling: Riutilizza connessioni HTTP
- Async/await: Per operazioni I/O intensive
- Validazione lato client: Valida dati prima di inviarli
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:
- Tempo medio ottimizzazione: Target < 5 minuti per 100 consegne
- Tasso di successo: Target > 95%
- Latenza API: Target < 500ms per richieste GET
- Errori 4xx/5xx: Monitora e allerta se > 1%
- Utilizzo rate limit: Evita di raggiungere il limite
10.5b Novità API (allineamento console)
Endpoint aggiuntivi rispetto al flusso base optimize → poll job (Bearer obbligatorio salvo diversa indicazione):
GET /api/v1/routing/eta— stima durata wall-clock dell'ottimizzazione.GET /api/v1/routing/history— cronologia run persistente.GET /api/v1/routing/calendar— eventi calendario (run + bozze).POST /api/v1/routing/simulate— what-if Control Tower da job baseline (patch: add/remove delivery/driver, TW, ecc.).GET /api/v1/routing/simulations/{simulation_id}— esito e confronto simulazione.POST /api/v1/routing/estimate-workload— stima fabbisogno minuti/driver (quick/precise).GET /api/v1/routing/delivery-execution/summarye.../jobs/{job_id}/delivery-execution— KPI esecuzione e POD.- Campo
savings_reportnel risultato job completato — risparmio stimato vs baseline.
10.6 Supporto e Contatti
Per assistenza tecnica o domande sull'integrazione:
- Email: [email protected]
Quando contatti il supporto, includi:
- Job ID (se disponibile)
- Messaggio di errore completo
- Payload della richiesta (senza dati sensibili)
- Log rilevanti
- Versione client API utilizzata