Logistivo CLI: gestisci la tua operatività logistica dal terminale e dal tuo ERP

Un unico catalogo di comandi alimenta tre superfici: il client da terminale lg, l'assistente IA all'interno della dashboard e il canale a cui si collegano gli assistenti IA esterni. Una funzionalità viene scritta una sola volta e compare in tutte e tre — e ora ogni azione di scrittura del pannello ha un comando, quindi i tuoi script e i tuoi agenti IA possono svolgere il lavoro che il tuo team fa a schermo. Pagamenti, consensi legali e chiusura dell'account restano deliberatamente nel browser. Questa pagina è un contratto. La tabella dei comandi qui sotto non è scritta a mano: viene generata dal registro live dei comandi a ogni richiesta.

Che cos'è la Logistivo CLI?

La Logistivo CLI (lg) è un client a riga di comando che svolge il lavoro del tuo account Logistivo — carichi e viaggi, richieste di trasporto, offerte e gare, flotta e autisti, documenti, contatti e partitari clienti e fornitori, fatture e contabilità, magazzino e scorte, membri del team e tariffe doganali — da un terminale, da uno script o dal tuo agente IA. I comandi non sono incorporati nel client: il client scarica il catalogo dei comandi dal server e si costruisce a partire da esso. Gli stessi comandi possono essere chiamati via HTTP senza installare nulla — è la strada usata dalle integrazioni ERP e CRM — e tramite il canale MCP a cui si collegano gli assistenti IA esterni.

Cosa è live oggi e cosa è in arrivo

Questa sezione è in cima alla pagina di proposito. L'errore più costoso che un documento di integrazione possa fare è descrivere come disponibile un endpoint non ancora aperto: l'integratore scrive il codice, l'endpoint risponde 404 e la fiducia non torna più.

Poiché il contratto è congelato, il codice di integrazione che scrivi oggi continua a funzionare man mano che si aprono i pezzi mancanti. I campi che non conosci vanno ignorati; è questa regola che ci permette di aggiungere senza rompere.

Cosa lo rende diverso: il client non conosce comandi, li disegna

È l'unica decisione che dà senso al resto della pagina. In una CLI ordinaria i comandi vivono dentro il client; in Logistivo vivono sul server.

Nell'impostazione classica, quando il server acquisisce una funzionalità il client ha bisogno di una nuova release e l'utente deve installarla. Nel frattempo le due parti sanno cose diverse — ed è la modalità di guasto più silenziosa nelle integrazioni: lo script gira, l'exit code è 0 e il comando semplicemente non descrive ciò che il server sa fare.

In Logistivo la definizione di un comando è un unico record: il nome, cosa fa, i parametri JSON Schema, quali ruoli possono vederlo, se richiede conferma e se il suo effetto è reversibile. All'avvio lg scarica quel catalogo e ne genera il testo di aiuto, i flag e la validazione dell'input. Nel momento in cui un comando viene aggiunto sul server, lg lo conosce — senza alcun aggiornamento del client.

Lo stesso catalogo alimenta l'assistente IA nella dashboard e il canale a cui si collegano gli assistenti IA esterni. Una funzionalità viene scritta una volta e compare in tre punti — e tutti e tre passano dallo stesso controllo del ruolo, dallo stesso controllo di conferma e dallo stesso registro di audit. Non esiste un inventario separato dei comandi CLI; se esistesse, entro sei mesi le tre superfici divergerebbero senza che nessuno se ne accorga.

La regola dei nomi — come crescono i nomi dei comandi

Il catalogo crescerà. I nuovi comandi non vengono nominati a caso; si applicano sei regole. Le pubblichiamo qui perché sono i nomi che la tua integrazione scriverà per anni, e la loro prevedibilità ha un valore per te.

Copertura: tutto ciò che c'è nel pannello, da un comando

Molti team collegano Logistivo al proprio agente IA o al proprio sistema di automazione, e un agente è utile solo se riesce a portare a termine il lavoro. Per questo la regola è semplice: tutto ciò che un utente d'ufficio può modificare nel pannello, può modificarlo anche un comando — dietro lo stesso controllo del ruolo, lo stesso controllo di conferma e lo stesso registro di audit.

È misurato, non affermato. Ogni endpoint di scrittura dietro il pannello viene confrontato con il registro dei comandi, e un endpoint conta come coperto solo quando un comando lo dichiara come proprio specchio HTTP o chiama lo stesso servizio condiviso. Degli endpoint di scrittura nel perimetro, il 100% è coperto e nessuno è privo di prova. Ogni endpoint escluso ha un motivo scritto — passaggi di identità e di pagamento, il control plane dell'integrazione, gli endpoint propri dell'app autisti, la telemetria del client, copie morte senza chiamanti — e quelli che contano per un integratore sono elencati qui sotto.

I comandi che accettano un file rientrano nella stessa regola: 33 comandi della tabella accettano un documento o una foto, e funzionano tutti da lg, via HTTP e via MCP (vedi Comandi che accettano file).

Anche il lavoro di gestione è coperto, dietro una chiave più forte: 62 comandi della tabella — membri del team e ruoli, impostazioni aziendali, conti bancari, carte salvate, chiavi di integrazione — richiedono una chiave di integrazione di livello manager. Una chiave di livello employee, quella predefinita, svolge l'operatività quotidiana e per quei comandi viene rifiutata con INSUFFICIENT_KEY_LEVEL.

Gli account affiliato (partner di segnalazione) hanno un catalogo tutto loro: 55 comandi in 20 domini — segnalazioni, lead, note e attività CRM, presentazioni di vendita, guadagni e pagamenti, più i comandi di account e team che ha ogni ruolo. I comandi riservati agli affiliati non sono nella tabella pubblica qui sotto, che elenca i tre ruoli del pannello; un account affiliato li vede in GET /api/common/commands e in lg commands. Un'azienda che è sia affiliata sia committente, autotrasportatore o doganalista vede entrambi gli insiemi.

Un agente dovrebbe iniziare con lg users get (nome API get_current_user): restituisce il ruolo, il livello di accesso della chiave in uso, i permessi effettivi e se un'approvazione legale in sospeso o un numero di telefono non verificato stanno bloccando le scritture — così l'agente può prevedere un rifiuto invece di collezionarlo.

La copertura è misurata da un report che confronta il registro dei comandi con gli endpoint di scrittura del pannello; un endpoint del pannello aggiunto senza un comando vi compare come privo di prova. Se trovi un'azione del pannello che non riesci a raggiungere da un comando, segnalala al supporto.

Installazione e identità

lg è uno script Node in un unico file, senza dipendenze (Node 18+), pubblicato su npm come @lgstv/lg. Il pacchetto installa due comandi identici, lg e logistivo — usa logistivo se sulla tua macchina lg è già un alias. Essendo un unico file, funziona anche copiare lg.mjs sul server ERP di un cliente. Non ti serve il client per costruire un'integrazione ERP/CRM — gli stessi comandi sono disponibili via HTTP.

1. Installa — Ti basta Node 18 o successivo; non ci sono altre dipendenze. Installalo globalmente oppure eseguilo senza installarlo tramite npx.

2. Accedi con un codice di approvazione — lg login mostra un breve codice di approvazione e apre logistivo.com/connect nel tuo browser; via SSH, in CI o con --no-browser stampa solo l'indirizzo, che puoi aprire su qualsiasi dispositivo, compreso il tuo telefono. Lì accedi come al solito, controlla che il codice sulla pagina sia lo stesso del tuo terminale, scegli l'accesso in sola lettura o in lettura e azione e approva. Nel terminale non si digita nulla di segreto: nessuna password, nessuna chiave, nessun codice monouso. La sessione resta poi attiva su quel computer — chiudere il terminale non ti disconnette, e lg rinnova da solo il token di accesso prima che scada. lg logout chiude la sessione anche sul server, e puoi rimuoverla nel pannello in Settings › Integration keys › AI and CLI connections. lg login --password (email e password) resta disponibile come alternativa di riserva. Richiede lg 0.11 o versioni successive.

3. Per server e CI: una chiave, non un codice di approvazione — Su un server o in un job di CI non c'è nessuno che possa approvare un codice in un browser. Emetti una chiave nel pannello (Settings › Integration keys) o con lg keys create — eventualmente per conto di un membro del team dedicato e con permessi ristretti — e mettila in LOGISTIVO_TOKEN, oppure salvala con lg login --api-key. Finché la variabile è impostata, il file di configurazione non viene mai letto. Dettagli: la sezione “Connettersi con una chiave API”.

4. Verifica ed esplora il catalogo — lg commands elenca tutti i comandi che il tuo ruolo può vedere, raggruppati per dominio; lg help stampa i verbi di quel dominio e lg help stampa l'elenco completo dei parametri di un comando, con i tipi e quelli obbligatori. Nessuno di questi testi vive dentro il client — tutto viene dal catalogo. Ricorda che l'elenco dipende dal tuo ruolo: i verbi di un dominio non sono gli stessi per un autotrasportatore e per un committente (loads set-status, per esempio, è solo nel catalogo dell'autotrasportatore). Se manca un verbo, controlla il tuo ruolo prima della tua installazione.

Non mettere mai la chiave nel controllo di versione, in un log della CI o in una finestra di chat. Se hai effettuato l'accesso con un codice di approvazione o una chiave di integrazione, lg logout chiude l'accesso anche sul server; in una sessione con password (lg login --password) cancella solo la copia LOCALE e lo dice esplicitamente. Il registro di audit memorizza i tuoi argomenti ma maschera come *** ogni campo il cui nome contiene token, password, IBAN, card o OTP, e ogni parametro che un comando dichiara segreto.

Ergonomia del client

Queste funzioni rendono più comodo l'uso quotidiano del client `lg`. Nessuna di esse modifica il contratto HTTP — salta questa sezione se ti integri direttamente via HTTP.

Connettersi con una chiave API

Un ERP, un CRM, uno script o un agente IA si collega a Logistivo con una chiave di integrazione emessa nel pannello o con lg keys create. La chiave agisce per conto di un vero membro del team: il ruolo di quella persona decide l'elenco dei comandi, il livello di accesso della chiave decide quanto può essere pesante il lavoro, e quando quella persona lascia il team la chiave muore con lei. Non esiste un “account di servizio” sintetico e non ce ne sarà mai uno — l'isolamento multi-tenant è legato all'utente, e un bridge senza proprietario è quello che continua a scrivere in silenzio per anni.

1. Emetti una chiave nel pannello — Settings › Integration keys › New key. Dalle un nome (“Sync notturna ERP”), scegli l'ambito e il livello di accesso e, se vuoi, accorcia la durata. La chiave viene mostrata una sola volta, solo in quella schermata; il server non la memorizza. Se la perdi, Rotate ne emette una nuova — quella vecchia diventa subito non valida.

2. Connettiti dal terminale — lg login --api-key convalida la chiave, stampa l'identità e l'ambito e scarica il catalogo dei comandi. lg whoami chiede al server con quale utente, azienda, ruolo e ambito stai operando. lg logout revoca la chiave anche sul server, così come vi chiude una sessione aperta con un codice di approvazione (solo il lg logout di una sessione con password non può farlo).

3. Oppure HTTP puro — La chiave è un token di accesso trasmesso nell'header Authorization: Bearer e funziona direttamente con i tre endpoint dei comandi. Su un server o in CI mettila in LOGISTIVO_TOKEN; finché quella variabile è impostata, il file di configurazione non viene mai letto.

4. Oppure gestisci le chiavi dal terminale: lg keys — lg keys è il gemello da terminale della schermata del pannello: stessi endpoint, stesso controllo (il permesso di gestione del team e, quando lo chiami con una chiave, una chiave di livello manager). create, rotate e transfer restituiscono un nuovo token una sola volta: con --save-profile=NAME finisce direttamente in un profilo locale e non viene mai stampato; altrimenti lg lo stampa una volta. In modalità --json il token è incluso solo se aggiungi --show-secret, e una chiamata --json senza nessuno dei due flag viene rifiutata prima che la richiesta parta, così il token non può finire in silenzio nell'output di uno script. rotate, transfer e revoke chiedono conferma (--yes); una chiave si indica con il suo nome esatto o con il suo uuid, mai per approssimazione. Il livello di accesso passa invariato a una chiave ruotata o trasferita.

L'ambito è applicato sull'intera superficie dell'API, non solo sul catalogo dei comandi: una chiave di sola lettura supera le richieste di lettura (GET/HEAD/OPTIONS) e ogni altra richiesta di scrittura risponde 403 forbidden. Ci sono due eccezioni deliberate, entrambe protette da un controllo più fine: l'endpoint di esecuzione dei comandi (la decisione viene dal flag read_only del comando stesso, perché anche i comandi che non scrivono si chiamano con POST) e una chiave che revoca se stessa (lg logout, consentito anche su una chiave di sola lettura). Ambito, autorizzazioni per dominio e livello di accesso sono tre assi indipendenti e devono essere superati tutti: una chiave read non può scrivere nemmeno a livello manager, una chiave write di livello employee non può gestire il team, e un'autorizzazione di dominio loads:write non apre il lavoro di gestione. Emetti la chiave per conto del membro con meno privilegi che copre le tue esigenze: è il ruolo a decidere quali record puoi vedere in assoluto.

I primi dieci minuti: dal pannello alla tua prima chiamata

I minuti più costosi di un'integrazione sono i primi dieci. I quattro passaggi qui sotto descrivono quel percorso in ordine; lo stesso ordine esiste nel pannello come procedura guidata (Settings › Integration › Setup wizard), e la procedura guidata chiama esattamente gli endpoint descritti qui. Uno sviluppatore senza accesso alla procedura guidata arriva allo stesso punto leggendo questa sezione.

Nessuno dei quattro passaggi finisce con “l'ho inviato”. Se la chiave funziona davvero lo dice il registro delle richieste; se il webhook arriva davvero lo dice il registro delle consegne. Entrambi sono consultabili nel pannello e in lg. Una configurazione mai misurata è una configurazione che si crede soltanto funzionante.

Sandbox: esercitati a scrivere senza toccare la produzione

Il preflight (dry_run) ti dice cosa FAREBBE un comando; ma se l'idempotenza riproduce davvero, se il webhook viene davvero consegnato, come appaiono i rami di errore e da dove arriva il credito lo si vede solo in una scrittura REALE. È a questo che serve la sandbox: un tenant separato che rispecchia la tua azienda — stesso codice, stesso catalogo di comandi — e non tocca mai il tuo registro live.

La sandbox non è un flag della richiesta, è un TENANT. Un'azienda Logistivo separata aperta per la tua: un proprio utente, un proprio pacchetto, un proprio registro dei crediti, proprie sottoscrizioni webhook. L'ambiente è DERIVATO dall'azienda della chiave e non può essere indicato da chi chiama, quindi anche se scegli il profilo sbagliato la richiesta si ferma lato server con 403 ENVIRONMENT_MISMATCH. “Pensavo di scrivere nella sandbox e ho scritto in produzione” è strutturalmente impossibile.

La superficie è identica — misurato: il catalogo che ottiene una chiave sandbox è lo stesso che ottiene la tua chiave live (con una chiave da committente, 448 comandi da entrambe le parti). Un bridge che funziona nella sandbox funziona in produzione; nulla di ciò che impari deve essere imparato due volte.

La sandbox non si apre vuota: contiene dati iniziali generati (NON copiati) — contatti, carichi in entrambe le direzioni e richieste di trasporto. I nomi sono dichiaratamente inventati e gli indirizzi email appartengono al dominio @sandbox.invalid; nessuna riga di un tenant reale viene mai letta, clonata o usata come modello.

Deliberatamente NON esiste un pulsante di reset, e nessun record, file o documento viene mai cancellato. Se vuoi ripartire da zero, il rinnovo apre una nuova generazione e quella vecchia resta, ritirata. Qualche centinaio di righe di prova accumulate non costa nulla rispetto a una routine di cancellazione collegata al tenant sbagliato.

Il modello dei comandi

Esiste una sola grammatica e non ha eccezioni. Per sapere come chiamare un comando ti serve soltanto ciò che il catalogo dice di lui.

lg loads list --status=in_transport --limit=20 --json

I nomi dei flag sono i nomi dei parametri dello schema, e lg accetta entrambe le grafie: --load_code e --load-code arrivano allo stesso campo (lg help stampa la forma kebab-case). Vedendo un flag sai quindi in quale campo JSON finisce, e passare dalla CLI a HTTP non richiede alcuna traduzione di nomi. Un parametro booleano scritto senza valore vale true (--only_open); il contrario è --no-only_open. Scrivi un argomento il cui valore inizia con un trattino come --flag=value, altrimenti un flag dimenticato ingoia in silenzio il successivo come proprio valore. Su un comando che accetta un file, metti @ davanti a un percorso locale (--attachment_uuid=@./cmr.pdf) — vedi Comandi che accettano file.

Esempi reali

Ogni comando qui sotto esiste oggi nel catalogo; nessun nome o parametro è inventato. Gli output sono abbreviati e mostrano il livello `data.result`; via HTTP lo stesso oggetto arriva dentro l'envelope (involucro di risposta) `{success, data:{…}}` (vedi l'API HTTP dei comandi).

Senza il client: l'API HTTP dei comandi

lg è una comodità, non la porta d'ingresso. Catalogo ed esecuzione passano entrambi da tre endpoint HTTP (i file ne hanno un quarto, l'endpoint di upload — vedi Comandi che accettano file); un'integrazione ERP o CRM si collega qui direttamente. L'identità è un token di accesso personale trasmesso in Authorization: Bearer. OGNI risposta arriva nello stesso envelope: `{ "success": bool, "data": { … }, "code": "…", "message": { … } }`. L'intero contratto vive dentro `data` — `contract_version`, `command`, `status` e, come output del comando, `result`; in caso di errore `error_code`, `error_key` ed `error`. Per risparmiare spazio, gli esempi di risposta in questa pagina mostrano l'oggetto `data` anziché l'envelope; non dimenticare di estrarre `data` nel tuo codice. Anche `lg --json` stampa l'envelope COMPLETO, ed è per questo che i percorsi jq iniziano con `.data.result…`. L'unica eccezione è `/api/public/status`: quell'endpoint è privo di envelope, così i sistemi di monitoraggio possono dare l'allarme senza analizzarlo.

Chiedere conferma con 200 + ok:false è vietato e non accadrà mai. Il motivo è semplice: i client trattano 200 come un successo, quindi una richiesta di conferma restituita con 200 bloccherebbe il flusso in silenzio e nessuno se ne accorgerebbe. La conferma è sempre 409.

Comandi che accettano file

33 comandi della tabella accettano un documento o una foto: allegare un CMR a un carico, leggere i codici HS da una fattura, creare un veicolo dal suo libretto di circolazione, stimare un pallet dalle foto, caricare un documento aziendale, confrontare una fattura con una packing list. Nel browser trascini il file in un modulo; altrove servono due passaggi — carica il file, poi passa il suo id al comando.

Un parametro accetta un file quando il suo nome termina in attachment_uuid o attachment_uuids — photos[].attachment_uuid dentro un elenco, front_attachment_uuid su un assegno. Non devi ricordartelo: ogni riga del catalogo riporta file_params con i percorsi esatti, lo schema contrassegna quei campi (format: uuid, x-logistivo-file: true) e la risposta del catalogo contiene un blocco attachments con l'indirizzo di upload, i tipi accettati, i limiti di dimensione e la durata di validità. Anche lg help li mostra.

Caricare è una scrittura, quindi una chiave con ambito di lettura non può caricare; qualsiasi chiave con ambito di scrittura può farlo, a entrambi i livelli di accesso. Gli upload appartengono all'utente, non alla chiave: due chiavi dello stesso utente vedono gli stessi upload, e nessuno può usare quelli di un altro. Un id sconosciuto, scaduto o non tuo semplicemente non viene trovato, e il motivo non viene rivelato.

Gli allegati della chat della dashboard e gli upload dei comandi sono spazi separati: un file allegato nella chat dell'assistente non può essere passato a un comando tramite id, e un upload di un comando non compare mai in un thread della chat.

Un upload non è un comando e da solo non ha alcun effetto di business: ai tuoi dati non succede nulla finché un comando non usa l'id. È anche per questo che caricare non chiede mai conferma — è il comando che consuma il file a portare il controllo.

Il controllo di conferma e i comandi irreversibili

Ogni comando del catalogo porta due flag: confirmation_needed (richiede un'approvazione esplicita prima di essere eseguito?) e irreversible (il suo effetto si può annullare?). Sono due domande distinte, ed entrambe sono applicate sul server invece di essere lasciate alla buona educazione del client.

Il controllo riguarda la responsabilità, non le regole di business. Presentare un'offerta è il primo passo verso un contratto di trasporto; emettere una fattura crea una scrittura contabile; un movimento di magazzino modifica un registro. Nessuna di queste cose dovrebbe mai poter essere eseguita due volte per sbaglio.

Il controllo si legge dal comando EFFETTIVO. Quando un comando ne avvolge un altro — come fa l'esecutore generico nella chat — il controllo deriva dai flag del comando che verrà davvero eseguito, non dall'involucro. Con un flag fisso uno dei due fallimenti sarebbe inevitabile: o il comando di fatturazione verrebbe eseguito senza conferma, oppure ogni ricerca di un paese farebbe comparire una scheda di conferma.

read_only e senza conferma non sono la stessa cosa. Esistono tre stati distinti: invoices preview non richiede conferma e non scrive nulla; loads create richiede conferma e scrive; invoices create non richiede conferma ma scrive — nasce una bozza. Lo decide un solo test: quando il comando termina, è cambiata una riga nel database?

Alcune azioni non sono dietro questo controllo perché non sono affatto comandi: acquistare un pacchetto, aggiungere una carta con 3D Secure, approvare i testi legali e chiudere un account avvengono nel browser della persona. Per queste, lg handoff-links generate restituisce un link a logistivo.com — gratuito, senza scrivere nulla, senza alcun segreto — e la persona completa il passaggio lì. Nessun flag, comunque impostato, trasforma un pagamento o un consenso in una chiamata API.

Tecnicamente un'integrazione può superare il controllo in automatico a ogni chiamata, ma non è consigliabile. Lo schema corretto è automatizzare il controllo solo per i comandi che sai essere reversibili e instradare quelli irreversibili nella coda di una persona.

Preflight: vedi cosa farebbe un comando di scrittura, senza eseguirlo

Il passaggio più costoso nel collegare un bridge ERP è scoprire se il tuo corpo è corretto eseguendo DAVVERO il comando: una bozza di fattura nel registro live, un credito bruciato, una pulizia manuale. dry_run mette fine a tutto questo — aggiungi un campo alla richiesta, il comando non viene eseguito e leggi cosa succederebbe.

Oggi il registro contiene 519 comandi; 233 di essi leggono soltanto e non scrivono nulla. Dei restanti 286 comandi di scrittura, 251 sono soggetti a conferma: chiamati senza confirm rispondono già 409 e descrivono cosa farebbero. La lacuna erano gli altri 35 — bozze di fattura, bozze di documenti di esportazione, righe: esattamente la catena che un bridge collega per prima. Per questi, chiamare significava eseguire.

C'era una seconda lacuna anche sui comandi soggetti a conferma: la validazione dello schema viene eseguita DOPO il controllo. Così una chiamata con un argomento mancante riceveva prima la scheda di conferma, la confermava, e solo allora si sentiva dire “manca questo campo”. Un preflight risponde a entrambe le cose in un solo giro.

E il bug di bridge più comune — e scoperto più tardi — è questo: invii un campo che non è nello schema (customer_id dove è atteso recipient_contact_id), il server lo scarta in silenzio, il comando risponde 200 e il record finisce nel posto sbagliato. La risposta del preflight elenca quei campi per nome sotto arguments.ignored; nessun messaggio di errore lo farà mai.

Un preflight non scrive nulla, e prima ancora di essere una misura è una proprietà strutturale: il percorso del preflight non entra mai nell'handler del comando, quindi vale per ogni comando, compresi quelli aggiunti in seguito. È stato anche misurato al rilascio: è stata catturata ogni istruzione SQL emessa durante il preflight di tutti i comandi di scrittura allora presenti nel catalogo (due insiemi di argomenti, due ruoli, 178 preflight). Nemmeno un INSERT/UPDATE/DELETE, nemmeno un job in coda, nemmeno un'email; il numero di righe di 31 tabelle e il saldo crediti sono rimasti invariati.

L'exit code è lo stesso che produrrebbe la chiamata reale — eseguibile 0, controllo di conferma 4, conflitto di idempotenza 2, qualsiasi altra cosa 1 — quindi "lg … --dry-run || exit" è un controllo significativo in uno step di CI.

Idempotenza: perché ritentare non crea duplicati

Un ERP non legge il tuo messaggio di errore, non vede il tuo schermo e reinvierà la stessa richiesta dopo un timeout. Questa sezione esiste esattamente per questo comportamento.

Una chiamata con un idempotency_key viene eseguita al massimo una volta. La seconda chiamata non riesegue il comando; restituisce il risultato registrato con stato replayed. Quando la rete cade, quando una richiesta va in timeout o quando una coda prende due volte lo stesso job, ricevi il primo risultato invece di una fattura duplicata.

Usa una chiave su ogni comando di scrittura. Il campo read_only del catalogo ti dà questa distinzione gratis: ogni comando con read_only: false merita una chiave di idempotenza.

La tua strategia di retry dovrebbe usare un backoff esponenziale e portare la STESSA chiave a ogni tentativo. Generare una chiave nuova per ogni tentativo equivale a non usare affatto l'idempotenza. Non inviare la stessa chiave in parallelo: l'esecuzione al massimo una volta è garantita per retry sequenziali, non per due richieste in competizione tra loro.

Sync incrementale e paginazione

Un bridge ERP/CRM dovrebbe scaricare ciò che è cambiato, non tutto ogni ora. Ogni comando di elenco — loads list, demands list-own, contacts list, invoices list, teammates list, load-groups list, reservations list, drivers list, tenders list, products search, warehouses search — accetta due parametri comuni e restituisce un campo comune.

Perché non c'è un offset: su una tabella live un offset scivola. Una riga inserita tra due pagine ti fa saltare o contare due volte un record, in silenzio. Il cursore va solo in avanti, quindi un record inserito o aggiornato a metà percorso finisce in fondo all'elenco: non si perde nulla, non si duplica nulla.

Un caricamento iniziale completo usa lo stesso meccanismo: parti da updated_since=1970-01-01T00:00:00Z e segui next_cursor finché non è null. Non esiste un endpoint di “esportazione” separato perché non serve.

Lo stesso con lg: lg loads list --updated_since=2026-09-01T00:00:00Z --limit=100 --json, poi lg loads list --cursor= --json.

Per la sync successiva imposta updated_since sul valore di updated_at più alto che hai visto. I timestamp hanno una precisione di un secondo, quindi un record sul confine può arrivare due volte — innocuo purché dalla tua parte tu faccia upsert per external_ref o code. Senza updated_since né cursor un comando gira in modalità chat: ordinamento pensato per le persone, al massimo 20 righe, next_cursor null e truncated che dice “c'è altro”. Non usare quella modalità per la sync.

Chiavi di errore leggibili dalle macchine per questa sezione: INVALID_CURSOR, INVALID_UPDATED_SINCE, DUPLICATE_EXTERNAL_REF, EXTERNAL_REF_TOO_LONG, EXTERNAL_REF_INVALID. Tutte rispondono HTTP 422 con error_code: "failed" e result.error_key nel corpo. Basa la logica sulla chiave, mai sul testo.

La tua chiave: external_ref

Non usare i nostri id auto-incrementali (invoice_id, contact_id) come chiave di business nel tuo ERP; sono opachi e privati della tua azienda. Carichi, richieste, contatti, fatture, prodotti e magazzini portano invece la tua chiave.

Webhook in uscita: quando succede qualcosa, facciamo noi una POST verso di te

Fin qui era tutto pull: tu chiedi, noi rispondiamo. Un bridge ERP doveva chiedere una volta al minuto per sapere che un carico aveva cambiato stato, che una fattura era stata emessa o che era arrivata un'offerta. Non più: registra un indirizzo https, scegli gli eventi che ti interessano e quando accadono arriva una POST firmata.

Il push è stato aggiunto ACCANTO al pull, non al suo posto. La sync incrementale (updated_since + cursore) è intatta ed è ancora lo strumento giusto: il primo caricamento completo, il recupero dello storico e la risposta a “mi sono perso qualcosa?” stanno tutti lì. I webhook riducono la latenza da minuti a secondi. Un bridge sano usa entrambi — il webhook come innesco, la sync come rete di sicurezza.

Il payload è un SEGNALE, non una copia del record. Contiene gli identificativi (id, il tuo external_ref, stato, orario); il dettaglio lo recuperi con la tua chiave via GET, e quella chiamata passa già dai tuoi controlli di autorizzazione. La superficie di esposizione resta quindi a livello di identificativi. Due campi mancano deliberatamente: l'identità dell'azienda che ha presentato un'offerta (le offerte cieche compaiono nel pannello con uno pseudonimo; mettere l'identità in un webhook infrangerebbe quella riservatezza attraverso un secondo canale) e i dati di contatto delle prenotazioni (non serve far passare dati personali da un canale in più).

I nomi degli eventi sono CONGELATI. Se il significato di un nome cambia, si apre un nome nuovo e quello vecchio sopravvive per una finestra di deprecazione. Aggiungere un nuovo campo al payload NON incrementa la versione del contratto — un consumatore deve ignorare i campi che non conosce.

Catalogo degli eventi

Ognuno dei 13 eventi qui sotto è coperto da una classe di evento EFFETTIVAMENTE emessa. Nel catalogo non ci sono nomi “che sarebbe bello avere”: un evento annunciato ma mai emesso è un evento che l'integratore aspetta per sempre, ed è un fallimento silenzioso. Neanche questa tabella è scritta a mano — viene ricavata dal catalogo live a ogni caricamento della pagina.

invoice.payment_recorded è deliberatamente ASSENTE: quella classe di evento esiste nel codice ma non viene emessa da nessuna parte. L'evento di pagamento verrà aggiunto il giorno in cui la classe verrà davvero emessa — non prima. Annunciare un evento in anticipo è peggio che non aggiungerlo mai.

Verificare la firma

Ogni richiesta porta un header X-Logistivo-Signature: t=,v1=. La stringa firmata è "." (il corpo grezzo) e l'algoritmo è HMAC-SHA256. Il prefisso v1 rende lo schema versionabile: se un giorno arrivasse un secondo schema, l'header riporterebbe t=…,v1=…,v2=… e tu sceglieresti quello che conosci.

Pubblichiamo due ricevitori di riferimento senza dipendenze, uno per Node e uno per PHP puro. Entrambi applicano le quattro regole qui sopra e si verificano da soli con --selftest: firma valida, corpo manomesso, segreto sbagliato, timestamp fuori finestra, header malformato, la trappola della riserializzazione ed entrambi i tipi di ripetizione (stessa consegna / stesso fatto). Il verificatore è l'esatta controparte del codice che usiamo noi stessi — consumiamo il contratto che pubblichiamo.

Consegna, tentativi e silenziamento

Il tuo ricevitore non sarà sempre attivo. Il sistema lo dà per scontato: un errore temporaneo entra nel piano dei tentativi, uno permanente silenzia la sottoscrizione — e il MOTIVO del silenziamento viene sempre registrato.

Il motivo del silenziamento è leggibile dalle macchine e stabile (delivery_failures, endpoint_gone, unsafe_target, key_revoked, by_user) e compare sia nel pannello sia in `lg webhooks list`. Un silenziamento senza motivo lascerebbe senza risposta la domanda “perché non arriva niente?”. La riattivazione azzera il contatore e il timbro.

Tu scrivi l'indirizzo; è il NOSTRO server a fare la richiesta. Senza un controllo, un tenant che inserisse l'indirizzo di metadati di un cloud potrebbe farci emettere richieste dall'interno della nostra rete. Per questo l'indirizzo viene verificato sia al momento della registrazione sia a OGNI consegna:

Salute e stato: nessuno dovrebbe accorgersi di un bridge silenzioso prima di te

Un bridge di integrazione raramente muore in modo rumoroso. Di solito si zittisce nelle ore piccole: un token scade, la persona per conto della quale agisce la chiave lascia il team, un ambito viene ristretto, oppure l'endpoint ricevente inizia a restituire 403. Il primo ad accorgersene è quasi sempre il cliente. lg status esiste per invertire quest'ordine.

Un solo comando risponde a due domande contemporaneamente, perché durante un'interruzione vengono poste entrambe nello stesso momento: la piattaforma è attiva, e il mio bridge è sano? La riga della piattaforma funziona senza token — durante un guasto il primo sospettato è proprio il token, e legare la domanda sullo stato al token ti lascerebbe senza risposta esattamente quando ti serve.

Il riepilogo di salute non tiene alcun registro nuovo; riunisce in un'unica frase tre fonti esistenti: il contatore di utilizzo giornaliero (quante chiamate hai fatto e quante sono fallite), il registro delle richieste (quale richiesta, quando e perché è stata rifiutata) e il registro delle consegne dei webhook in uscita (l'evento è arrivato al ricevitore?). Il tasso di errore viene sempre letto dal contatore giornaliero; il registro di dettaglio ha un limite e lo dice chiaramente quando il limite viene raggiunto.

Lo stesso riepilogo compare nel pannello: in Settings › Integration › Keys ogni riga ha un badge e, sotto, il motivo. Le soglie non vengono calcolate lato client; il livello assegnato dal server è quello che mostrano sia il pannello sia lg status, quindi le due schermate non possono mai dire cose diverse sulla stessa chiave.

Il tuo sistema di monitoraggio non ha bisogno di un token. L'endpoint restituisce solo parole di stato; non contiene versione, nome host, nome della coda, numero di job, testo degli errori o dati dei clienti — e non può contenerli.

Il riepilogo di salute viene prodotto con un numero costante di query per richiesta; il numero di query non cresce con il numero di chiavi. Per il dettaglio, leggi le richieste recenti di quella chiave con lg logs e il bridge in uscita con lg webhooks list.

Errori ed exit code

Ciò su cui una macchina basa le sue decisioni è una stringa stabile. Il testo nel campo error è rivolto a una persona, è localizzato e cambia senza preavviso — un'integrazione che decide in base al testo del messaggio non è supportata, e il fatto che si rompa non è un bug.

Due tassonomie, due campi. error_code è il livello di trasporto (la tabella qui sopra) ed è il primo su cui basare la logica. error_key è la chiave stabile della regola di business dentro failed — INSUFFICIENT_CREDITS, NOT_FOUND, DUPLICATE_CONTACT, INSUFFICIENT_STOCK — pubblicata come dizionario nel catalogo (error_keys) e qui sotto. Un comando che non emette ancora una chiave restituisce error_key: null; tratta una chiave assente dal dizionario come un fallimento generico.

Promessa di stabilità

La domanda più costosa in un contratto di integrazione è “il codice che scrivo oggi funzionerà ancora domani?”. Risponde un solo numero: contract_version nel catalogo, e l'header X-Contract-Version in ogni risposta. Risponde esattamente a una domanda — il codice scritto in precedenza funziona ancora senza modifiche? Se il numero non è aumentato, sì. Se è aumentato, no: qualcosa su cui facevi affidamento è stato rimosso o ristretto.

Perché un annuncio non rimuove nulla: il codice esistente funziona esattamente come prima, anche il giorno dopo. Incrementare la versione all'annuncio emetterebbe due segnali per una sola modifica e ti abituerebbe a ignorare il primo — indebolendo il segnale che conta davvero. L'annuncio viaggia già sulla chiamata che stai facendo: deprecated_at e replaced_by sulla riga del catalogo, un header X-Deprecated più un blocco deprecated nella risposta di quel comando, e un avviso di una riga su stderr se usi lg. Non devi fare nulla per sapere che qualcosa sta per sparire; è già nel tuo log.

Tra l'annuncio e la rimozione passano almeno due trimestri di calendario. Per tutta la finestra il vecchio comando risponde ancora, e avvisa a ogni chiamata. Quando un nome CLI viene ritirato, il suo successore ne eredita il nome come alias: durante la finestra il vecchio nome raggiunge il vecchio comando, e il giorno in cui viene rimosso la stessa chiamata arriva a quello nuovo — senza che tu debba rilasciare una nuova versione del client.

preview_invoice → get_invoice (annunciato il 3 settembre 2026). get_invoice restituisce lo stesso blocco di anteprima e aggiunge le righe, il dettaglio delle imposte, i pagamenti registrati, il saldo residuo e l'accesso tramite la chiave del tuo ERP (external_ref). preview_invoice continua a funzionare, così come lg invoices preview — entrambi avvisano.

Tabella delle identità dei comandi

La tabella qui sotto non è scritta a mano: viene generata dal registro live dei comandi ogni volta che questa pagina viene servita. Un comando aggiunto sul server compare qui da solo. La colonna “Controllo” ti dice se un comando chiede conferma e se il suo effetto può essere annullato; la colonna “Ruoli” ti dice quali tipi di account lo troveranno nel loro catalogo. I badge segnalano i comandi che richiedono una chiave di livello manager, quelli che accettano un file e quelli che si eseguono solo da lg.

Il nome API (list_loads) è congelato e non cambierà; è il nome che usi nelle chiamate HTTP. L'identità CLI (loads list) è regolata dalla politica di versioning: una volta annunciata cambia solo con un incremento di versione e una finestra di migrazione, durante la quale il catalogo serve la vecchia coppia come alias. Le descrizioni qui provengono dal record di ciascun comando e sono abbreviate — per il testo completo, i tipi dei parametri e i valori enum, leggi l'endpoint del catalogo oppure esegui lg help . I comandi riservati agli affiliati non sono elencati qui; un account affiliato li vede nel proprio catalogo.

Integrazione ERP e CRM — Identità: nessun account di servizio, ma un utente reale

Un ERP o un CRM si collega a Logistivo come un vero utente aziendale che possiede un token di accesso personale generato dal pannello. Un account di servizio sintetico — senza azienda, esente dai limiti di ambito, senza nessun responsabile — non viene mai creato.

È una questione di architettura, non di preferenza. L'isolamento multi-tenant si basa interamente sull'azienda dell'utente; per un'identità senza azienda l'ambito di isolamento diventa inerte, e una sola query sbagliata con quel token legge dati che non le appartengono. Il secondo motivo è l'audit: la colonna utente nel registro collega “chi l'ha eseguito” a una persona; un account di servizio rende quella colonna priva di significato e lascia senza proprietario un'integrazione che emette fatture. Il terzo è la revoca: se un token appartiene a una persona, l'integrazione si zittisce quando quella persona se ne va. Un bridge senza proprietario è il bridge che continua a scrivere in silenzio per anni.

Integrazione ERP e CRM — Versioning e deprecazione

La risposta del catalogo riporta un intero contract_version; le righe dei comandi riportano since, deprecated_at e replaced_by. La versione del contratto aumenta solo con le modifiche incompatibili (breaking change).

Integrazione ERP e CRM — Identificativi stabili: non dare mai al tuo ERP un id auto-incrementale come chiave

Gli id interni auto-incrementali sono opachi e limitati al tenant: l'id valido di un'altra azienda per te restituisce “non trovato”, non dati. Ma essere opachi non li rende chiavi di business. Ciò che un ERP memorizza nel proprio record deve essere una chiave di business.

Integrazione ERP e CRM — Paginazione, filtri e sync incrementale

I comandi di elenco accettano limit e restituiscono count. Per una sync notturna il contratto si impegna su quanto segue; un comando di elenco che lo ha adottato dichiara cursor e updated_since nel suo JSON Schema, e finché non lo fa trattalo come una finestra piccola (massimo 20 righe, nessun cursore).

Integrazione ERP e CRM — Rate limit

La chiave del limite è il token, non l'IP: un ERP arriva da dietro un unico NAT, e un limite basato sull'IP conterebbe un'intera azienda come un solo utente. Due integrazioni con due chiavi non si consumano mai la quota a vicenda — il secondo motivo di “un'integrazione per chiave”.

Integrazione ERP e CRM — Cose che non faremo

Spesso è la parte più utile di un contratto: sapere cosa non arriverà mai ti permette di progettare di conseguenza.

Il catalogo leggibile dalle macchine

Questa pagina è stata scritta per le persone. Per un assistente IA o un client automatico, le stesse informazioni esistono come copia strutturata e senza autenticazione.

GET /api/public/cli/catalog non richiede autenticazione e restituisce l'identità pubblica dei comandi: il nome API congelato, l'identità CLI dominio/verbo, la descrizione, i parametri JSON Schema, i flag read_only, confirmation_needed e irreversible, il credit_cost e i campi di accesso key_level, permission, file_params, secret_parameters e cli_only. Non restituisce dati personali, dati di tenant né record di esempio — solo com'è fatta la superficie.

Il modo giusto in cui un agente esterno può usarlo: leggere il catalogo, spiegare con PAROLE PROPRIE quali lavori sono possibili e, quando serve un'esecuzione, indirizzare l'utente a eseguirla nel proprio ambiente con il proprio token. L'agente non chiede, non genera e non inoltra mai il token di un utente.

Collegare Logistivo a un assistente IA per la registrazione e le richieste di trasporto è un canale separato, con una pagina dedicata:

La definizione leggibile dalle macchine: OpenAPI 3.1 e Postman

Il catalogo qui sopra è il formato proprio di Logistivo. Le stesse informazioni sono pubblicate anche in due formati standard del settore che i tuoi strumenti comprendono già: una definizione OpenAPI 3.1 e una collection Postman. Nessuna delle due è scritta a mano — entrambe vengono generate dal registro live dei comandi a ogni richiesta, quindi non possono diventare obsolete.

La definizione È il catalogo, non una seconda realtà. Ogni comando compare come operazione POST /api/common/commands/; lo schema del corpo è lo JSON Schema del comando stesso (byte per byte ciò che vede l'assistente), lo schema della risposta è l'envelope comune, i flag di controllo (read_only, confirmation_needed, irreversible e il badge dei crediti) stanno nell'estensione x-logistivo, e le chiavi di errore di business stanno sotto x-error-keys. Un comando ritirato è contrassegnato con deprecated: true e resta richiamabile per tutta la sua finestra di sei mesi.

Accanto alla superficie dei comandi, nella definizione ci sono anche gli endpoint REST che portano il contratto di integrazione: 10 endpoint di elenco con sync incrementale (updated_since + cursor + limit) e 7 endpoint di scrittura che accettano la tua chiave (external_ref). La superficie REST di Logistivo è molto più ampia; il resto è escluso DI PROPOSITO. Quei payload sono il contratto proprio del prodotto, e uno schema scritto per supposizione romperebbe il tuo client generato alla prima chiamata — e tu cercheresti il bug nel tuo codice, non nel nostro.

La definizione viene generata per il RUOLO di chi la richiede: il comando di un altro ruolo non entra mai nel documento, perché una definizione è un contratto, non un dizionario, e tutto ciò che contiene deve essere richiamabile con quel token. La versione senza autenticazione è l'unione dei tre ruoli, e lì ogni operazione indica sotto x-logistivo.roles quali ruoli la vedono.

Non fidarti della nostra parola — verifica: npx @redocly/cli lint logistivo-openapi.json passa senza alcun avviso.

Generazione di codice: un client tipizzato dalla definizione

Hai la definizione; ora genera da essa codice funzionante. `lg codegen` legge il documento OpenAPI e scrive un client in un unico file, senza dipendenze — TypeScript (dichiarazioni di tipo più un sottile wrapper per le chiamate) oppure PHP (una classe client con array shape PHPDoc). Niente `npm install`, niente composer: `fetch` lato TypeScript, `ext-curl` lato PHP.

La generazione avviene sulla TUA macchina, non sul nostro server. Non è una preferenza: fissa una volta la definizione nel tuo repository (`lg spec --out=...`) e lo stesso comando gira a ogni build senza rete e senza token, così la tua CI non dipende mai dalla nostra disponibilità. E poiché il codice generato è codice che GIRA nel tuo ERP, tenerlo nel tuo repository invece di scaricarlo da un'API significa che ogni modifica compare in `git diff`.

Il client generato gestisce correttamente tutti e tre i canali di Logistivo, e capisce quale è quale dal documento invece di indovinarlo: ogni operazione dichiara il proprio canale in `x-logistivo.surface`. Il canale dei comandi estrae il contenuto dall'envelope `{success, data:{…}}`; un elenco di sync REST NON è avvolto in un envelope e viene restituito così com'è (estrarlo farebbe perdere `next_cursor`); una scrittura REST restituisce il corpo di prodotto proprio dell'endpoint. Un generatore generico non conosce questa distinzione — il nostro sì.

I tipi non sono congetture: l'interfaccia degli argomenti di ogni comando viene generata proprio dallo JSON Schema che vede l'assistente. Ciò che è obbligatorio resta obbligatorio, gli enum diventano union type, gli oggetti annidati diventano tipi annidati. In pratica: se il server richiede un campo, la chiamata che lo dimentica non fallisce con un 422 in produzione — fallisce in fase di compilazione.

Nel file non viene scritto alcun timestamp di generazione: la stessa definizione con gli stessi flag produce gli stessi byte. Rigenerare quindi non fa rumore in `git diff`, e l'unica cosa davvero cambiata — il contratto — resta visibile.

Perimetro onesto

Preferiamo far crescere questa sezione piuttosto che ridurla. Il valore di un contratto di integrazione sta meno in ciò che promette e più nella chiarezza con cui dice ciò che non fa.

Domande frequenti

A cosa serve la Logistivo CLI?

Svolge il lavoro del tuo account Logistivo da un terminale, da uno script o dal tuo agente IA: carichi e viaggi, richieste di trasporto, offerte e gare, flotta, autisti e le loro spese, documenti, contatti e partitari clienti e fornitori, fatture e contabilità, magazzino e scorte, membri del team, tariffe HS e dazi antidumping. Ogni azione di scrittura del pannello ha un comando; pagamenti, consensi legali e chiusura dell'account restano deliberatamente nel browser. Gli stessi comandi sono richiamabili via HTTP per le integrazioni ERP/CRM e tramite il canale MCP.

Come sono nominati i comandi nella Logistivo CLI?

La forma è lg --parameter=value. Il dominio è un'entità al plurale (loads, demands, invoices, export-documents) e il verbo proviene da un vocabolario chiuso che cresce solo per decisione esplicita (list, get, create, update, search, set-status, generate e così via). Tutti i verbi di un'entità vivono in un solo dominio, e la coppia (dominio, verbo) è unica in tutto il catalogo.

Come mi autentico nella CLI?

In modo interattivo, lg login mostra un codice di approvazione che confermi nel browser dopo aver effettuato l'accesso a Logistivo — nel terminale non si digita né una password né una chiave, e la sessione resta attiva e si rinnova da sola finché non esegui lg logout. lg login --password (email e password) resta disponibile come alternativa di riserva. Sui server e in CI, usa una chiave di integrazione emessa nel pannello o con lg keys create: lg login --api-key la salva in un profilo locale, oppure mettila nella variabile d'ambiente LOGISTIVO_TOKEN. Via HTTP il token viaggia nell'header Authorization: Bearer.

Posso creare un account di servizio per la mia integrazione ERP?

No. Un'integrazione si collega con il token di un vero utente aziendale creato nel pannello. Un'identità sintetica senza azienda rende inerte l'isolamento multi-tenant, rende priva di significato la colonna “chi l'ha eseguito” nel registro di audit e lascia un bridge che non si zittisce quando il suo proprietario se ne va. Lo schema corretto è un utente dedicato con un ruolo ristretto e un proprietario con nome e cognome.

Logistivo ha un ambiente di test (sandbox)?

Sì, ed è self-service: per la tua azienda viene aperto un tenant sandbox separato — stesso codice, stesso catalogo di comandi, un proprio registro dei crediti. L'ambiente è derivato dall'azienda della chiave e non può essere indicato nella richiesta; una chiamata fatta con il profilo sbagliato si ferma lato server con 403 ENVIRONMENT_MISMATCH. Una richiesta di trasporto aperta nella sandbox non raggiunge mai autotrasportatori reali, un evento della sandbox non viene mai consegnato alla tua sottoscrizione webhook live e il tuo saldo live non si muove. Endpoint: POST /api/common/integration-sandbox (apertura), POST /api/common/integration-sandbox/keys (chiave), GET /api/common/integration-sandbox/go-live (checklist di go-live). Dal lato di lg, lg --sandbox e lg --live sono selettori di profilo validi per una singola esecuzione.

Come leggo l'output di lg --json in uno script?

L'output è l'intero envelope HTTP: { "success": bool, "data": { "contract_version", "command", "status", "result" }, "code", "message" }. Il payload di business sta sotto .data.result; nei comandi di elenco le righe sono .data.result., il conteggio è .data.result.count e la pagina successiva è .data.result.next_cursor. Quindi il percorso jq corretto è simile a lg loads list --json | jq -r '.data.result.loads[].load_code'. Un percorso che salta l'envelope non dà errore, restituisce null in silenzio — in uno script di monitoraggio diventa un allarme che non smette mai di suonare. L'unica eccezione è /api/public/status, il cui corpo è deliberatamente privo di envelope.

Se invio due volte la stessa richiesta, ottengo record duplicati?

No, se hai inviato un idempotency_key. Una seconda chiamata con la stessa chiave non riesegue il comando; restituisce il primo risultato con stato replayed. La chiave dovrebbe essere un UUID e deve restare la STESSA tra un retry e l'altro — generare una chiave nuova per ogni tentativo equivale a non usare l'idempotenza.

Cosa fa --yes?

Supera il controllo di conferma. Quando un comando soggetto a conferma viene chiamato senza --yes non cambia nulla: il server riassume in una frase cosa sta per fare, dice se è irreversibile e si ferma (HTTP 409, exit code della CLI 4). Ripetere il comando con --yes esegue il lavoro.

Come rilevo un comando fallito in uno script?

Leggi l'exit code, mai il testo del messaggio: 0 successo, 1 rifiuto per regola di business, 2 errore di comando o di argomento, 3 errore di autenticazione o di ruolo, 4 conferma richiesta. Via HTTP gli equivalenti sono 200, 422, 404, 403 e 409, e il campo error_code nel corpo è stabile. Il MESSAGGIO di errore è localizzato e cambia senza preavviso.

Come scopro i comandi disponibili in modo programmatico?

GET /api/public/cli/catalog per la discovery senza autenticazione, e GET /api/common/commands per l'elenco che il tuo account può effettivamente vedere. Entrambi restituiscono i parametri JSON Schema di ogni comando; costruisci il tuo client a partire da questi invece di scrivere nel codice un elenco fisso di comandi.

Devo aggiornare il mio client quando viene aggiunto un nuovo comando?

No. Il client scarica il suo elenco di comandi dal catalogo, quindi un comando aggiunto sul server compare senza una nuova release del client. Un aggiornamento serve solo per una modifica incompatibile che incrementa la versione del contratto, e in quel caso la vecchia forma resta attiva per almeno sei mesi.

La CLI sostituisce la REST API esistente?

No. La superficie REST che alimenta le app web e mobile resta al suo posto. Il catalogo dei comandi non sostituisce il CRUD delle risorse; è il livello dei verbi — l'unico modo, protetto dal controllo del ruolo, dal controllo di conferma e sottoposto ad audit, di eseguire un'azione.

Come ottengo una chiave API Logistivo e cosa può fare?

La emette un responsabile con il permesso di gestione del team, nel pannello in Settings › Integration keys oppure da un terminale con lg keys create. La chiave agisce per conto di un vero membro del team e ha tre impostazioni indipendenti: l'ambito (read apre il catalogo e i comandi che non scrivono, write apre tutti i comandi — il controllo di conferma resta com'è), autorizzazioni facoltative per dominio e un livello di accesso — employee, il predefinito, per l'operatività quotidiana, oppure manager per team, impostazioni, chiavi e denaro. Viene mostrata una sola volta e non viene memorizzata sul server; può essere ruotata, trasferita e revocata. Quando il membro per conto del quale agisce se ne va, anche la chiave si chiude. Non esiste un account di servizio.

Cosa succede se riutilizzo una chiave di idempotenza per un comando diverso?

Il server risponde 409 idempotency_conflict e indica il comando per cui la chiave è stata usata la prima volta; non viene eseguito né riprodotto nulla. Le chiavi sono limitate alla tua azienda, legate a un solo comando e bloccate per 24 ore — dopo di che la stessa chiave avvia una nuova esecuzione.

Come vengono applicati i rate limit alla mia integrazione?

Per token, al minuto, in tre bucket: letture del catalogo 120, comandi di lettura 120, comandi di scrittura 30. Un 429 riporta sempre Retry-After e un corpo con retry_after, limit e bucket; attendi quel tempo e riprova. Il catalogo pubblica i valori attuali sotto rate_limits.

Come distinguo un rifiuto di business da un errore di trasporto?

Basa la logica prima su error_code: failed (422) è un rifiuto di business e riporta un error_key stabile come INSUFFICIENT_CREDITS o NOT_FOUND; server_error (5xx) e gli errori di rete si ritentano con la stessa chiave di idempotenza; per rate_limited (429) si attende. Non decidere mai in base al testo dell'errore.

Logistivo invia webhook? Per quali eventi?

Sì. Registri un indirizzo https e scegli gli eventi che ti interessano; quando accadono arriva una POST firmata. Oggi sono sottoscrivibili 13 eventi: carico creato/cambio di stato/autista assegnato, richiesta di trasporto creata e cambio di stato, offerta presentata/rivista/accettata, fattura in bozza/emessa/annullata, e prenotazione di magazzino creata/cambio di stato. Ogni nome è coperto da una classe di evento effettivamente emessa — nulla viene annunciato per poi non essere mai emesso, perché un evento del genere è un fallimento silenzioso che l'integratore aspetta per sempre. Le sottoscrizioni si creano in Settings › Integration › Webhooks, oppure da un terminale con lg webhooks create.

Come verifico la firma di un webhook Logistivo?

Ogni richiesta porta un header X-Logistivo-Signature: t=,v1=. La stringa firmata è ".", l'algoritmo è HMAC-SHA256 e la chiave è il segreto della sottoscrizione. Quattro regole: (1) verifica i byte GREZZI — decodificare e riserializzare il corpo (JSON.stringify / json_encode) trasforma gli stessi dati in byte diversi e la firma smette di corrispondere; è l'errore più comune in assoluto; (2) controlla il timestamp e scarta tutto ciò per cui |now − t| > 300 s anche se la firma è corretta; (3) confronta in tempo costante con hash_equals / timingSafeEqual; (4) deduplica su event_id, non sull'id della consegna — event_id viene mantenuto nel replay mentre l'id della consegna cambia. Ricevitori Node e PHP funzionanti si trovano in examples/webhook-receiver/ nel repository e si verificano da soli con --selftest.

Cosa succede se un webhook non può essere consegnato? Perdo l'evento?

No. Se il tuo ricevitore non restituisce 2xx la consegna entra nel piano dei tentativi: 60 · 300 · 1800 · 7200 · 21600 secondi, 6 tentativi in totale, una finestra di circa 8,6 ore. OGNI errore tranne 410 Gone viene ritentato, 4xx compresi — perdere un evento di business costa più di qualche richiesta in più. Dopo 10 consegne consecutive esaurite la sottoscrizione viene silenziata e il motivo viene registrato, così la domanda “perché non arriva niente?” ha sempre una risposta. Il registro delle consegne conserva ogni tentativo (codice di stato, durata, errore) e puoi reinviare una singola consegna dal pannello o con lg webhooks replay. La sync incrementale (updated_since + cursore) è ancora lì come rete di sicurezza.

Il mio agente IA può fare tutto ciò che un utente può fare nel pannello?

Quasi tutto. Ogni azione di scrittura del pannello ha un comando — misurato sugli endpoint di scrittura del pannello, il 100% di quelli nel perimetro è coperto — e gli stessi comandi sono aperti a lg, a HTTP e al canale MCP. Le eccezioni deliberate sono i passaggi di identità e di denaro: l'accesso, l'acquisto di un pacchetto, l'aggiunta di una carta con 3D Secure, l'approvazione dei testi legali e la chiusura di un account restano nel browser della persona, e lg handoff-links generate restituisce il link per essi. Chiavi di integrazione, webhook e sandbox si gestiscono con i comandi integrati di lg perché restituiscono segreti una sola volta.

Posso passare file come PDF o foto ai comandi dalla CLI o dall'API?

Sì. Con lg, metti @ davanti a un percorso locale (--attachment_uuid=@./cmr.pdf) e lg carica prima il file; --file è una scorciatoia quando un comando ha un solo campo file, e lg upload si limita a caricare. Via HTTP, invia il file come multipart a POST /api/common/commands:upload e passa l'attachment_uuid restituito al comando; i client MCP usano lo strumento upload_attachment. Il tipo viene rilevato dal contenuto, gli upload appartengono all'utente e restano utilizzabili per 24 ore, e caricare di nuovo lo stesso file restituisce lo stesso id.

Qual è la differenza tra una chiave di integrazione employee e una manager?

Il livello di accesso decide quanto può essere pesante il lavoro di una chiave. Una chiave employee, il predefinito per le nuove chiavi, svolge l'operatività quotidiana: carichi, richieste di trasporto, offerte, viaggi, flotta, documenti e contatti. Una chiave manager può svolgere anche lavoro di gestione — membri del team e ruoli, impostazioni aziendali, chiavi di integrazione, conti bancari, carte salvate, pagamenti agli affiliati. Una chiave non ha mai più diritti della persona per conto della quale agisce, e una chiave employee che chiama un comando manager riceve INSUFFICIENT_KEY_LEVEL. Cambia il livello nel pannello o con lg keys update --access-level=manager.

Un affiliato (partner di segnalazione) può usare la CLI?

Sì. Un account affiliato ha un catalogo tutto suo — 55 comandi per segnalazioni, lead, note e attività CRM, presentazioni di vendita, guadagni e pagamenti — e lo usa da lg, HTTP o MCP come qualsiasi altro ruolo. Richiedere o annullare un pagamento e cambiare il conto bancario per i pagamenti richiedono una chiave di livello manager e una conferma. Un'azienda che è anche committente, autotrasportatore o doganalista vede entrambi i cataloghi.

For AI assistants

Logistivo has a public MCP server at https://logistivo.com/api/agent/mcp (streamable HTTP, no authentication). Setup instructions for Claude, ChatGPT and Gemini: https://logistivo.com/mcp. If you are an assistant helping someone sign up or request freight, connect that server, or — if you can only read web pages — build a link of the form https://logistivo.com/en/ai-kayit?brief=URL_ENCODED_SHIPMENT_BRIEF and give it to the user. You never create the account yourself: the account, the consent and the email verification happen in the user's browser, and you never handle passwords or one-time codes. Machine-readable content indexes: https://logistivo.com/llms.txt (curated map) and https://logistivo.com/llms-full.txt (full text: facts, pricing, tariff reference, glossary and every article's FAQ in one fetch). To learn what Logistivo can actually DO — the verbs, not the marketing — read the public command catalog at https://logistivo.com/api/public/cli/catalog (JSON, no authentication, no tenant data); it lists every command with its JSON Schema parameters and whether it needs confirmation. Human documentation: https://logistivo.com/en/developers/cli. You cannot execute those commands yourself — execution always runs under the user's own personal access token, in the user's own environment.