Logistivo CLI: stuur je logistieke operatie aan vanuit een terminal en vanuit je ERP

Eén commandocatalogus voedt drie oppervlakken: de lg-terminalclient, de AI-assistent in het dashboard en het kanaal waarmee externe AI-assistenten verbinding maken. Een functie wordt één keer geschreven en verschijnt in alle drie — en elke schrijfactie in het paneel heeft nu een commando, zodat je scripts en je eigen AI-agents het werk kunnen doen dat je team op het scherm doet. Betalingen, juridische toestemming en het sluiten van een account blijven bewust in de browser. Deze pagina is een contract. De commandotabel hieronder is niet met de hand geschreven; ze wordt bij elk verzoek gegenereerd uit het live commandoregister.

Wat is de Logistivo CLI?

De Logistivo CLI (lg) is een opdrachtregelclient die het werk in je Logistivo-account uitvoert — ladingen en ritten, aanvragen, biedingen en tenders, wagenpark en chauffeurs, documenten, contacten en rekeningen-courant, facturen en boekhouding, magazijn en voorraad, teamleden en douanetarieven — vanuit een terminal, een script of je eigen AI-agent. De commando's zitten niet in de client ingebakken: de client haalt de commandocatalogus op van de server en bouwt zichzelf daaruit op. Dezelfde commando's zijn via HTTP aan te roepen zonder iets te installeren — dat is de route die ERP- en CRM-integraties gebruiken — en via het MCP-kanaal waarmee externe AI-assistenten verbinding maken.

Wat er vandaag live is, en wat eraan komt

Deze sectie staat met opzet bovenaan de pagina. De duurste fout die een integratiedocument kan maken, is een endpoint dat nog niet open is beschrijven alsof het dat wel is: de integrator schrijft de code, het endpoint geeft 404 terug, en het vertrouwen komt niet meer terug.

Omdat het contract bevroren is, blijft de integratiecode die je vandaag schrijft werken terwijl de resterende onderdelen opengaan. Velden die je niet kent, moeten worden genegeerd; die regel is wat ons in staat stelt toe te voegen zonder iets te breken.

Wat dit anders maakt: de client kent geen commando's, hij bouwt ze op

Dit is de ene beslissing die de rest van de pagina betekenis geeft. In een gewone CLI wonen de commando's in de client; bij Logistivo wonen ze op de server.

In de klassieke opzet heeft de client, wanneer de server een nieuwe functie krijgt, een nieuwe release nodig en moet de gebruiker die installeren. In de tussentijd weten de twee kanten verschillende dingen — en dat is de stilste faalwijze bij integraties: het script draait, de exitcode is 0, en het commando beschrijft gewoon niet wat de server kan.

Bij Logistivo is de definitie van een commando één enkel record: de naam, wat het doet, de JSON Schema-parameters, welke rollen het mogen zien, of het bevestiging nodig heeft en of het effect omkeerbaar is. Wanneer lg start, haalt het die catalogus op en genereert het daaruit de helptekst, de vlaggen en de invoervalidatie. Zodra er op de server een commando wordt toegevoegd, kent lg het — zonder client-update.

Dezelfde catalogus voedt de AI-assistent in het dashboard en het kanaal waarmee externe AI-assistenten verbinding maken. Een functie wordt één keer geschreven en verschijnt op drie plekken — en alle drie gaan door dezelfde rolcontrole, dezelfde bevestigingspoort en hetzelfde auditlogboek. Er is geen aparte inventaris van CLI-commando's; als die er was, zouden de drie oppervlakken binnen zes maanden ongemerkt uit elkaar groeien.

De naamgevingsregel — hoe commandonamen groeien

De catalogus zal groeien. Nieuwe commando's krijgen geen naam op goed geluk; er worden zes regels toegepast. We publiceren ze hier omdat dit de namen zijn die je integratie jarenlang zal schrijven, en hun voorspelbaarheid is iets waard voor je.

Dekking: alles in het paneel, vanuit een commando

Veel teams koppelen Logistivo aan hun eigen AI-agent of automatiseringsharnas, en een agent is alleen nuttig als hij het werk kan afmaken. Daarom is de regel eenvoudig: wat een kantoorgebruiker in het paneel kan wijzigen, kan een commando ook wijzigen — achter dezelfde rolcontrole, dezelfde bevestigingspoort en hetzelfde auditlogboek.

Dit is gemeten, niet beweerd. Elk schrijf-endpoint achter het paneel wordt gecontroleerd tegen het commandoregister, en een endpoint telt alleen als gedekt wanneer een commando het als zijn HTTP-spiegel declareert of dezelfde gedeelde service aanroept. Van de schrijf-endpoints binnen de scope is 100% gedekt en geen enkel onbewezen. Elk weggelaten endpoint heeft een schriftelijke reden — stappen rond identiteit en betaling, het control plane van de integratie, de eigen endpoints van de chauffeursapp, clienttelemetrie, dode kopieën zonder aanroeper — en de endpoints die voor een integrator van belang zijn, staan hieronder.

Commando's die een bestand aannemen, vallen onder dezelfde regel: 33 commando's in de tabel nemen een document of foto aan, en ze werken allemaal vanuit lg, via HTTP en via MCP (zie Commando's die bestanden aannemen).

Ook beheerwerk is gedekt, achter een sterkere sleutel: 62 commando's in de tabel — teamleden en rollen, bedrijfsinstellingen, bankrekeningen, opgeslagen kaarten, integratiesleutels — vereisen een integratiesleutel op managerniveau. Een sleutel op employee-niveau, de standaard, voert dagelijkse operaties uit en wordt bij die commando's geweigerd met INSUFFICIENT_KEY_LEVEL.

Affiliate-accounts (doorverwijspartners) hebben een eigen catalogus: 55 commando's in 20 domeinen — doorverwijzingen, leads, CRM-notities en -taken, salespresentaties, verdiensten en uitbetalingen, plus de account- en teamcommando's die elke rol heeft. De commando's die alleen voor affiliates zijn, staan niet in de publieke tabel hieronder, die de drie paneelrollen toont; een affiliate-account ziet ze in GET /api/common/commands en in lg commands. Een bedrijf dat zowel affiliate als verlader, vervoerder of douane-expediteur is, ziet beide sets.

Een agent moet beginnen met lg users get (API-naam get_current_user): dat geeft de rol, het toegangsniveau van de gebruikte sleutel, de effectieve rechten en of een openstaande juridische goedkeuring of een niet-geverifieerd telefoonnummer op dit moment schrijfacties blokkeert — zodat de agent een weigering kan voorspellen in plaats van haar te verzamelen.

Dekking wordt gemeten met een rapport dat het commandoregister vergelijkt met de schrijf-endpoints van het paneel; een paneel-endpoint dat zonder commando wordt toegevoegd, verschijnt daarin als onbewezen. Vind je een actie in het paneel die je niet vanuit een commando kunt bereiken, meld die dan bij support.

Installatie en identiteit

lg is een Node-script van één bestand zonder afhankelijkheden (Node 18+), gepubliceerd op npm als @lgstv/lg. Het pakket installeert twee identieke commando's, lg en logistivo — gebruik logistivo als lg op je machine al een alias is. Omdat het één bestand is, werkt het ook om lg.mjs naar de ERP-server van een klant te kopiëren. Je hebt de client niet nodig om een ERP/CRM-integratie te bouwen — dezelfde commando's zijn beschikbaar via HTTP.

1. Installeren — Node 18 of nieuwer is alles wat je nodig hebt; er zijn geen andere afhankelijkheden. Installeer het globaal of draai het zonder installatie via npx.

2. Inloggen met een goedkeuringscode — lg login toont een korte goedkeuringscode en opent logistivo.com/connect in je browser; via SSH, in CI of met --no-browser drukt het alleen het adres af, dat je op elk apparaat kunt openen, ook op je telefoon. Log daar zoals gewoonlijk in, controleer of de code op de pagina dezelfde is als die in je terminal, kies toegang voor alleen lezen of voor lezen en handelen, en keur goed. Er wordt niets geheims in de terminal getypt: geen wachtwoord, geen sleutel, geen eenmalige code. De sessie blijft daarna op die computer ingelogd — de terminal sluiten logt je niet uit, en lg vernieuwt het access token zelf voordat het verloopt. lg logout beëindigt de sessie ook op de server, en je kunt haar in het paneel verwijderen onder Settings › Integration keys › AI and CLI connections. lg login --password (e-mailadres en wachtwoord) blijft beschikbaar als terugvaloptie. Vereist lg 0.11 of nieuwer.

3. Voor servers en CI: een sleutel, geen goedkeuringscode — Op een server of in een CI-job is er niemand die een code in een browser kan goedkeuren. Geef een sleutel uit in het paneel (Settings › Integration keys) of met lg keys create — eventueel namens een speciaal daarvoor bestemd teamlid met beperkte rechten — en zet hem in LOGISTIVO_TOKEN, of sla hem op met lg login --api-key. Zolang de variabele is gezet, wordt het configuratiebestand nooit gelezen. Details: de sectie "Verbinden met een API-sleutel".

4. Controleren en de catalogus verkennen — lg commands toont elk commando dat je rol kan zien, gegroepeerd per domein; lg help toont de werkwoorden van dat domein, en lg help toont de volledige parameterlijst van één commando met de types en welke parameters verplicht zijn. Geen van die teksten zit in de client — alles komt uit de catalogus. Onthoud dat de lijst afhangt van je rol: de werkwoorden van één domein zijn niet dezelfde voor een vervoerder en voor een verlader (loads set-status staat bijvoorbeeld alleen in de vervoerderscatalogus). Ontbreekt er een werkwoord, controleer dan eerst je rol voordat je je installatie controleert.

Zet de sleutel nooit in versiebeheer, een CI-log of een chatvenster. Als je met een goedkeuringscode of een integratiesleutel bent ingelogd, beëindigt lg logout de sessie ook op de server; in een wachtwoordsessie (lg login --password) verwijdert het alleen de LOKALE kopie en zegt het dat ook expliciet. Het auditlogboek slaat je argumenten op, maar maskeert elk veld waarvan de naam token, password, IBAN, card of OTP bevat als ***, evenals elke parameter die een commando als geheim declareert.

Ergonomie van de client

Deze functies maken het dagelijks gebruik van de `lg`-client makkelijker. Geen ervan verandert het HTTP-contract — sla deze sectie over als je rechtstreeks via HTTP integreert.

Verbinden met een API-sleutel

Een ERP, CRM, script of AI-agent maakt verbinding met Logistivo via een integratiesleutel die is uitgegeven in het paneel of met lg keys create. De sleutel handelt namens een echt teamlid: de rol van die persoon bepaalt de commandolijst, het toegangsniveau van de sleutel bepaalt hoe zwaar het werk mag zijn, en wanneer die persoon het team verlaat, sterft de sleutel mee. Er is geen synthetisch "serviceaccount" en dat komt er ook niet — multi-tenant-isolatie is gebonden aan de gebruiker, en een koppeling zonder eigenaar is het soort dat jarenlang stilletjes blijft schrijven.

1. Geef een sleutel uit in het paneel — Settings › Integration keys › New key. Geef hem een naam ("ERP nachtelijke sync"), kies de scope en het toegangsniveau, en verkort eventueel de looptijd. De sleutel wordt één keer getoond, alleen op dat scherm; de server slaat hem niet op. Ben je hem kwijt, dan geeft Rotate een nieuwe uit — de oude wordt onmiddellijk ongeldig.

2. Verbinden vanuit de terminal — lg login --api-key valideert de sleutel, toont de identiteit en de scope, en downloadt de commandocatalogus. lg whoami vraagt de server als welke gebruiker, met welk bedrijf, welke rol en welke scope je draait. lg logout trekt de sleutel ook op de server in, net zoals het daar een sessie met een goedkeuringscode beëindigt (alleen de lg logout van een wachtwoordsessie kan dat niet).

3. Of gewoon HTTP — De sleutel is een access token dat in de header Authorization: Bearer wordt meegestuurd en werkt rechtstreeks tegen de drie commando-endpoints. Zet hem op een server of in CI in LOGISTIVO_TOKEN; zolang die variabele is gezet, wordt het configuratiebestand nooit gelezen.

4. Of beheer sleutels vanuit de terminal: lg keys — lg keys is de terminaltweeling van het paneelscherm: dezelfde endpoints, dezelfde controle (het recht voor teambeheer, en een sleutel op managerniveau wanneer je het met een sleutel aanroept). create, rotate en transfer geven één keer een nieuw token terug: met --save-profile=NAME gaat het rechtstreeks naar een lokaal profiel en wordt het nooit afgedrukt; anders drukt lg het één keer af. In de modus --json wordt het token alleen opgenomen als je --show-secret toevoegt, en een aanroep met --json zonder een van beide vlaggen wordt geweigerd voordat het verzoek wordt verstuurd, zodat het token niet ongemerkt in de uitvoer van een script kan belanden. rotate, transfer en revoke vragen om bevestiging (--yes); een sleutel wordt aangesproken met zijn exacte naam of zijn uuid, nooit met een gok. Het toegangsniveau gaat ongewijzigd over op een geroteerde of overgedragen sleutel.

De scope wordt gehandhaafd over het hele API-oppervlak, niet alleen in de commandocatalogus: een alleen-lezen-sleutel laat leesverzoeken (GET/HEAD/OPTIONS) door en elk ander schrijvend verzoek antwoordt met 403 forbidden. Er zijn twee bewuste uitzonderingen en beide worden bewaakt door een fijnere controle: het endpoint voor commando-uitvoering (de beslissing komt uit de eigen vlag read_only van het commando, omdat ook niet-schrijvende commando's met POST worden aangeroepen) en een sleutel die zichzelf intrekt (lg logout, ook toegestaan met een alleen-lezen-sleutel). Scope, domeinrechten en toegangsniveau zijn drie onafhankelijke assen en ze moeten alle drie slagen: een leessleutel kan zelfs op managerniveau niet schrijven, een schrijfsleutel op employee-niveau kan het team niet beheren, en een domeinrecht loads:write opent geen beheerwerk. Geef de sleutel uit namens het lid met de minste rechten dat aan je behoeften voldoet: de rol bepaalt welke records je überhaupt kunt zien.

De eerste tien minuten: van het paneel naar je eerste aanroep

De duurste minuten van een integratie zijn de eerste tien. De vier stappen hieronder zetten dat pad op volgorde; dezelfde volgorde bestaat in het paneel als wizard (Settings › Integration › Setup wizard), en de wizard roept precies de endpoints aan die hier worden beschreven. Een ontwikkelaar zonder toegang tot de wizard komt op dezelfde plek uit door deze sectie te lezen.

Geen van de vier stappen eindigt met "ik heb het verstuurd". Of de sleutel echt werkt, wordt beantwoord door het verzoekenlogboek; of de webhook echt aankomt, door het afleveringslogboek. Beide zijn open in het paneel en in lg. Een setup die nooit wordt gemeten, is een setup waarvan alleen wordt geloofd dat hij werkt.

Sandbox: schrijven oefenen zonder productie aan te raken

Preflight (dry_run) vertelt je wat een commando ZOU doen; maar of idempotency echt een replay geeft, of de webhook echt wordt afgeleverd, hoe de foutvertakkingen eruitzien en waar de credits vandaan komen, zie je alleen bij een ECHTE schrijfactie. Daar is de sandbox voor: een aparte tenant die je bedrijf spiegelt — dezelfde code, dezelfde commandocatalogus — en die nooit je live administratie aanraakt.

De sandbox is geen vlag in het verzoek, het is een TENANT. Een apart Logistivo-bedrijf dat voor het jouwe wordt geopend: een eigen gebruiker, een eigen pakket, een eigen creditadministratie, eigen webhookabonnementen. De omgeving wordt AFGELEID van het bedrijf van de sleutel en kan niet door de aanroeper worden meegegeven, dus zelfs als je het verkeerde profiel kiest, stopt het verzoek aan de serverkant met 403 ENVIRONMENT_MISMATCH. "Ik dacht dat ik naar de sandbox schreef en schreef naar productie" is structureel onmogelijk.

Het oppervlak is identiek — gemeten: de catalogus die een sandboxsleutel ophaalt, is dezelfde als die van je livesleutel (bij een verladerssleutel 448 commando's aan beide kanten). Een koppeling die in de sandbox werkt, werkt in productie; niets wat je leert, hoef je twee keer te leren.

De sandbox opent niet leeg: hij bevat gegenereerde (NIET gekopieerde) seeddata — contacten, ladingen in beide richtingen en aanvragen. De namen zijn openlijk verzonnen en de e-mailadressen vallen onder het domein @sandbox.invalid; er wordt nooit een rij van een echte tenant gelezen, gekloond of als sjabloon gebruikt.

Er is bewust GEEN resetknop, en er wordt nooit een record, bestand of document verwijderd. Wil je een schone start, dan opent een vernieuwing een nieuwe generatie en blijft de oude bestaan, buiten gebruik. Een paar honderd opgestapelde oefenrijen kosten niets vergeleken met een verwijderroutine die aan de verkeerde tenant is gekoppeld.

Het commandomodel

Er is één grammatica en die kent geen uitzonderingen. Om te weten hoe je een commando aanroept, heb je alleen nodig wat de catalogus erover zegt.

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

Vlagnamen zijn de parameternamen uit het schema, en lg accepteert beide schrijfwijzen: --load_code en --load-code bereiken hetzelfde veld (lg help toont de kebab-case-vorm). Aan een vlag zie je dus in welk JSON-veld hij terechtkomt, en de overstap van de CLI naar HTTP vraagt geen naamvertaling. Een booleaanse parameter zonder waarde is true (--only_open); het tegenovergestelde is --no-only_open. Schrijf een argument waarvan de waarde met een streepje begint als --flag=value, anders slikt een vergeten vlag stilzwijgend de volgende in als waarde. Bij een commando dat een bestand aanneemt, zet je @ voor een lokaal pad (--attachment_uuid=@./cmr.pdf) — zie Commando's die bestanden aannemen.

Echte voorbeelden

Elk commando hieronder bestaat vandaag in de catalogus; geen enkele naam of parameter hier is verzonnen. De uitvoer is ingekort. De uitvoer toont het niveau `data.result`; via HTTP komt hetzelfde object binnen in de envelop `{success, data:{…}}` (zie de HTTP-commando-API).

Zonder de client: de HTTP-commando-API

lg is een gemak, niet de toegangsdeur. Catalogus en uitvoering lopen allebei via drie HTTP-endpoints (bestanden hebben een vierde, het upload-endpoint — zie Commando's die bestanden aannemen); een ERP- of CRM-integratie maakt hier rechtstreeks verbinding. De identiteit is een persoonlijk access token dat wordt meegestuurd in Authorization: Bearer. ELK antwoord komt terug in dezelfde envelop: `{ "success": bool, "data": { … }, "code": "…", "message": { … } }`. Het hele contract zit in `data` — `contract_version`, `command`, `status` en, als uitvoer van het commando, `result`; bij een fout `error_code`, `error_key` en `error`. De antwoordvoorbeelden op deze pagina tonen het object `data` in plaats van de envelop, om ruimte te besparen; vergeet niet om in je eigen code `data` uit te pakken. Ook `lg --json` drukt de HELE envelop af, en daarom beginnen jq-paden met `.data.result…`. De enige uitzondering is `/api/public/status`: dat endpoint is envelopvrij, zodat watchdogs kunnen alarmeren zonder het te parseren.

Om bevestiging vragen met 200 + ok:false is verboden en zal nooit gebeuren. De reden is eenvoudig: clients behandelen 200 als succes, dus een bevestigingsverzoek dat als 200 terugkomt, zou de flow stilletjes doden en niemand zou het merken. Bevestiging is altijd 409.

Commando's die bestanden aannemen

33 commando's in de tabel nemen een document of foto aan: een CMR aan een lading koppelen, HS-codes uit een factuur lezen, een voertuig aanmaken op basis van het kentekenbewijs, een pallet schatten op basis van foto's, een bedrijfsdocument uploaden, een factuur vergelijken met een paklijst. In de browser sleep je het bestand naar een formulier; overal elders kost het twee stappen — het bestand uploaden en daarna de id ervan aan het commando doorgeven.

Een parameter neemt een bestand aan wanneer de naam eindigt op attachment_uuid of attachment_uuids — photos[].attachment_uuid binnen een lijst, front_attachment_uuid bij een cheque. Dat hoef je niet te onthouden: elke catalogusrij bevat file_params met de exacte paden, het schema markeert die velden (format: uuid, x-logistivo-file: true), en het catalogusantwoord bevat een blok attachments met het uploadadres, de geaccepteerde types, de groottelimieten en de geldigheidsduur. lg help toont ze ook.

Uploaden is een schrijfactie, dus een sleutel met leesscope kan niet uploaden; elke sleutel met schrijfscope kan het wel, op beide toegangsniveaus. Uploads horen bij de gebruiker, niet bij de sleutel: twee sleutels van dezelfde gebruiker zien dezelfde uploads, en niemand kan die van iemand anders gebruiken. Een id die onbekend, verlopen of niet van jou is, wordt simpelweg niet gevonden, en de reden wordt niet bekendgemaakt.

Bijlagen in de dashboardchat en commando-uploads zijn gescheiden ruimtes: een bestand dat in de assistentchat is bijgevoegd, kan niet via de id aan een commando worden doorgegeven, en een commando-upload verschijnt nooit in een chatgesprek.

Een upload is geen commando en heeft op zichzelf geen zakelijk effect: er gebeurt niets met je gegevens totdat een commando de id gebruikt. Daarom vraagt uploaden ook nooit om bevestiging — het commando dat het bestand gebruikt, draagt de poort.

De bevestigingspoort en onomkeerbare commando's

Elk commando in de catalogus draagt twee vlaggen: confirmation_needed (is er expliciete goedkeuring nodig voordat het draait) en irreversible (kan het effect ongedaan worden gemaakt). Het zijn afzonderlijke vragen, en beide worden op de server gehandhaafd in plaats van overgelaten aan de goede manieren van de client.

De poort gaat over verantwoordelijkheid, niet over bedrijfsregels. Een bod uitbrengen is de eerste stap naar een vrachtcontract; een factuur uitgeven maakt een boeking in de boekhouding aan; een voorraadmutatie wijzigt een grootboek. Geen van deze mag ooit iets zijn dat je per ongeluk twee keer kunt uitvoeren.

De poort wordt gelezen van het EFFECTIEVE commando. Wanneer het ene commando het andere omhult — zoals de generieke uitvoerder in de chat doet — komt de poort uit de vlaggen van het commando dat daadwerkelijk zal draaien, niet van de wrapper. Met een vaste vlag zou een van twee fouten onvermijdelijk zijn: óf het factuurcommando zou onbevestigd draaien, óf elke opzoeking van een land zou een bevestigingskaart oproepen.

read_only en niet bevestigingsplichtig zijn niet hetzelfde. Er zijn drie verschillende toestanden: invoices preview is niet bevestigingsplichtig en schrijft niets; loads create is bevestigingsplichtig en schrijft; invoices create is niet bevestigingsplichtig maar schrijft wel — er ontstaat een concept. Eén test beslist: is er, wanneer het commando terugkeert, een rij in de database veranderd?

Sommige acties zitten niet achter deze poort, omdat het helemaal geen commando's zijn: een pakket kopen, een kaart toevoegen met 3D Secure, juridische teksten goedkeuren en een account sluiten gebeuren in de eigen browser van de persoon. Daarvoor geeft lg handoff-links generate een logistivo.com-link terug — gratis, zonder iets te schrijven, zonder geheim — en de persoon rondt de stap daar af. Geen enkele vlag, hoe die ook staat, maakt van een betaling of een toestemming een API-aanroep.

Technisch kan een integratie de poort bij elke aanroep automatisch passeren, maar dat is niet aan te raden. Het juiste patroon is de poort alleen te automatiseren voor commando's waarvan je weet dat ze omkeerbaar zijn, en onomkeerbare commando's naar de wachtrij van een mens te leiden.

Preflight: zien wat een schrijfcommando zou doen, zonder het uit te voeren

De duurste stap bij het aansluiten van een ERP-koppeling is uitvinden of je body klopt door het commando ECHT uit te voeren: een conceptfactuur in het live grootboek, een verbrande credit, handmatig opruimen. dry_run maakt daar een einde aan — je voegt één veld toe aan het verzoek, het commando draait niet, en je leest wat er zou gebeuren.

Het register bevat vandaag 519 commando's; 233 daarvan lezen alleen en schrijven niets. Van de overige 286 schrijfcommando's zijn er 251 bevestigingsplichtig: zonder confirm aangeroepen antwoorden ze al met 409 en beschrijven ze wat ze zouden doen. Het gat zat in de andere 35 — factuurconcepten, concepten van exportdocumenten, regelitems: precies de keten die een koppeling als eerste aansluit. Voor die commando's was aanroepen hetzelfde als uitvoeren.

Zelfs bij bevestigingsplichtige commando's was er een tweede gat: schemavalidatie draait NA de poort. Een aanroep met een ontbrekend argument kreeg dus eerst de bevestigingskaart, bevestigde die, en hoorde pas daarna "dit veld ontbreekt". Een preflight beantwoordt beide in één round trip.

En de meest voorkomende — en het laatst ontdekte — koppelingsbug is deze: je stuurt een veld dat niet in het schema staat (customer_id waar recipient_contact_id wordt verwacht), de server laat het stilzwijgend vallen, het commando geeft 200 terug en het record belandt op de verkeerde plek. Het preflight-antwoord noemt die velden bij naam onder arguments.ignored; een foutmelding zal dat nooit doen.

Een preflight schrijft niets, en dat is structureel nog voordat het een meting is: het preflightpad komt nooit in de handler van het commando, dus het geldt voor elk commando, ook voor de later toegevoegde. Het werd bij de lancering ook gemeten: elk SQL-statement dat werd uitgevoerd tijdens het preflighten van elk schrijfcommando dat toen in de catalogus stond (twee sets argumenten, twee rollen, 178 preflights), werd vastgelegd. Geen enkele INSERT/UPDATE/DELETE, geen enkele job in de wachtrij, geen enkele e-mail; het aantal rijen in 31 tabellen en het creditsaldo bleven ongewijzigd.

De exitcode is dezelfde die de echte aanroep zou geven — zou draaien 0, bevestigingspoort 4, idempotency-conflict 2, al het andere 1 — dus "lg … --dry-run || exit" is een zinvolle poort in een CI-stap.

Idempotency: waarom opnieuw proberen geen duplicaten oplevert

Een ERP leest je foutmelding niet, ziet je scherm niet en stuurt na een timeout hetzelfde verzoek opnieuw. Deze sectie bestaat precies voor dat gedrag.

Een aanroep met een idempotency_key draait hoogstens één keer. De tweede aanroep voert het commando niet opnieuw uit; hij geeft het vastgelegde resultaat terug met status replayed. Valt het netwerk weg, verloopt een verzoek of pakt een wachtrij dezelfde job twee keer op, dan krijg je het eerste resultaat in plaats van een dubbele factuur.

Gebruik een sleutel bij elk schrijvend commando. Het veld read_only in de catalogus geeft je dat onderscheid gratis: elk commando met read_only: false verdient een idempotency key.

Je retrystrategie moet exponential backoff gebruiken en bij elke poging DEZELFDE sleutel meesturen. Per poging een nieuwe sleutel genereren is hetzelfde als helemaal geen idempotency gebruiken. Stuur dezelfde sleutel niet parallel: at-most-once is gegarandeerd voor opeenvolgende retries, niet voor twee verzoeken die met elkaar racen.

Incrementele sync en paginering

Een ERP/CRM-koppeling moet ophalen wat er veranderd is, niet elk uur alles. Elk lijstcommando — loads list, demands list-own, contacts list, invoices list, teammates list, load-groups list, reservations list, drivers list, tenders list, products search, warehouses search — accepteert twee gedeelde parameters en geeft één gedeeld veld terug.

Waarom er geen offset is: op een live tabel verschuift een offset. Een rij die tussen twee pagina's wordt ingevoegd, laat je een record overslaan of dubbel tellen, zonder dat je het merkt. De cursor gaat alleen vooruit, dus een record dat halverwege wordt ingevoegd of bijgewerkt, belandt aan het eind van de lijst: er gaat niets verloren en niets wordt gedupliceerd.

Een volledige eerste import is hetzelfde mechanisme: begin met updated_since=1970-01-01T00:00:00Z en volg next_cursor totdat die null is. Er is geen apart “export”-endpoint, omdat dat niet nodig is.

Hetzelfde met lg: lg loads list --updated_since=2026-09-01T00:00:00Z --limit=100 --json, daarna lg loads list --cursor= --json.

Zet voor de volgende sync updated_since op de grootste updated_at die je hebt gezien. Tijdstempels hebben een precisie van één seconde, dus een record op de grens kan twee keer binnenkomen — onschuldig zolang je aan jouw kant upsert op external_ref of code. Zonder updated_since of cursor draait een commando in chatmodus: menselijke ordening, hoogstens 20 rijen, next_cursor null en truncated die zegt “er is meer”. Gebruik die modus niet voor sync.

Machineleesbare foutsleutels voor deze sectie: INVALID_CURSOR, INVALID_UPDATED_SINCE, DUPLICATE_EXTERNAL_REF, EXTERNAL_REF_TOO_LONG, EXTERNAL_REF_INVALID. Ze antwoorden allemaal met HTTP 422 met error_code: "failed" en result.error_key in de body. Vertak op de sleutel, nooit op de tekst.

Je eigen sleutel: external_ref

Maak onze auto-increment-id's (invoice_id, contact_id) niet de bedrijfssleutel in je ERP; ze zijn ondoorzichtig en privé voor je bedrijf. Ladingen, aanvragen, contacten, facturen, producten en magazijnen dragen in plaats daarvan jouw sleutel.

Uitgaande webhooks: wij sturen je een POST wanneer er iets gebeurt

Alles tot hier was pull: jij vraagt, wij antwoorden. Een ERP-koppeling moest elke minuut vragen om te weten te komen dat een lading van status was veranderd, een factuur was uitgegeven of een bod was binnengekomen. Nu niet meer: registreer een https-adres, kies de events die je belangrijk vindt, en er komt een ondertekende POST binnen wanneer ze gebeuren.

Push werd NAAST pull toegevoegd, niet in plaats ervan. Incrementele sync (updated_since + cursor) is onaangetast en nog steeds het juiste gereedschap: de eerste volledige import, het aanvullen van historie en het beantwoorden van "heb ik iets gemist" gebeuren daar allemaal. Webhooks brengen de latency terug van minuten naar seconden. Een gezonde koppeling gebruikt beide — de webhook als trigger, de sync als vangnet.

De payload is een SIGNAAL, geen kopie van het record. Hij bevat de identifiers (id, je eigen external_ref, status, tijd); de details haal je op met je eigen sleutel via GET, en die aanroep gaat al door je eigen autorisatiecontroles. Het blootstellingsoppervlak blijft dus op het niveau van identifiers. Twee velden ontbreken bewust: de identiteit van het bedrijf dat een bod heeft uitgebracht (blinde biedingen verschijnen in het paneel onder een alias; de identiteit in een webhook opnemen zou die privacy via een tweede kanaal doorbreken) en de contactgegevens van reserveringen (er is geen reden om persoonsgegevens via een extra kanaal te verplaatsen).

Eventnamen zijn BEVROREN. Verandert de betekenis van een naam, dan wordt er een nieuwe naam geopend en leeft de oude door tijdens een uitfaseringsperiode. Een nieuw veld aan de payload toevoegen verhoogt de contractversie NIET — een consument moet velden negeren die hij niet kent.

Eventcatalogus

Elk van de 13 events hieronder wordt gedekt door een eventklasse die ECHT wordt verzonden. Er staan geen namen van het type "zou leuk zijn" in de catalogus: een event dat wordt aangekondigd maar nooit afgaat, is een event waar de integrator eeuwig op wacht, en die fout is stil. Ook deze tabel is niet met de hand geschreven — hij wordt bij elke paginaweergave uit de live catalogus opgebouwd.

invoice.payment_recorded ontbreekt BEWUST: die eventklasse bestaat in de code, maar wordt nergens vandaan verzonden. Het betalingsevent wordt toegevoegd op de dag dat de klasse echt wordt afgevuurd — niet eerder. Een event te vroeg aankondigen is erger dan het nooit toevoegen.

De handtekening verifiëren

Elk verzoek bevat een header X-Logistivo-Signature: t=,v1=. De ondertekende string is "." en het algoritme is HMAC-SHA256. Het voorvoegsel v1 houdt het schema versioneerbaar: komt er ooit een tweede schema, dan bevat de header t=…,v1=…,v2=… en kies je degene die je kent.

We publiceren twee referentie-ontvangers zonder afhankelijkheden, één voor Node en één voor gewoon PHP. Beide passen de vier regels hierboven toe en testen zichzelf met --selftest: geldige handtekening, gemanipuleerde body, verkeerd secret, tijdstempel buiten het venster, misvormde header, de valkuil van het opnieuw serialiseren en beide soorten herhaling (dezelfde aflevering / hetzelfde feit). De verifier is de exacte tegenhanger van de code die we zelf gebruiken — we consumeren het contract dat we publiceren.

Aflevering, retries en stillegging

Je ontvanger zal niet altijd bereikbaar zijn. Het spoor gaat daarvan uit: een tijdelijke fout komt in het retryschema, een permanente fout legt het abonnement stil — en de REDEN van stillegging wordt altijd vastgelegd.

De reden van stillegging is machineleesbaar en stabiel (delivery_failures, endpoint_gone, unsafe_target, key_revoked, by_user) en verschijnt zowel in het paneel als in `lg webhooks list`. Stilleggen zonder reden zou de vraag "waarom komt er niets binnen" onbeantwoordbaar maken. Opnieuw inschakelen zet de teller en de stempel terug.

Jij schrijft het adres; ONZE server doet het verzoek. Zonder controle zou een tenant die een metadata-adres van een cloud invoert, ons verzoeken kunnen laten doen vanuit ons eigen netwerk. Daarom wordt het adres gecontroleerd bij registratie en bij ELKE aflevering:

Gezondheid en status: niemand zou een stille koppeling eerder moeten opmerken dan jij

Een integratiekoppeling sterft zelden met veel lawaai. Meestal wordt ze stil in de kleine uurtjes: een token verloopt, de persoon namens wie de sleutel draait, verlaat het team, een scope wordt versmald, of het ontvangende endpoint begint 403 terug te geven. Wie het als eerste merkt, is bijna altijd de klant. lg status bestaat om die volgorde om te draaien.

Eén commando beantwoordt twee vragen tegelijk, omdat tijdens een storing beide op hetzelfde moment worden gesteld: is het platform in de lucht, en is mijn koppeling gezond. De platformregel werkt zonder token — tijdens een storing is het token zelf de eerste verdachte, en de statusvraag daaraan koppelen zou je zonder antwoord laten precies wanneer je er een nodig hebt.

De gezondheidssamenvatting houdt geen nieuw logboek bij; ze vat drie bestaande bronnen samen in één zin: de dagelijkse gebruiksteller (hoeveel je hebt aangeroepen en hoeveel er mislukte), het verzoekenlogboek (welk verzoek, wanneer, en waarom het werd geweigerd) en het afleveringslogboek van de uitgaande webhooks (heeft het event de ontvanger bereikt). Het foutpercentage wordt altijd uit de dagelijkse teller gelezen; het detaillogboek is begrensd en zegt dat duidelijk wanneer de grens is bereikt.

Dezelfde samenvatting verschijnt in het paneel: onder Settings › Integration › Keys heeft elke rij een badge met daaronder de reden. Drempels worden niet aan de clientkant berekend; welk niveau de server ook toekent, dat tonen zowel het paneel als lg status, zodat de twee schermen nooit verschillende dingen over dezelfde sleutel kunnen zeggen.

Je watchdog heeft geen token nodig. Het endpoint geeft alleen statuswoorden terug; het bevat geen versie, hostnaam, wachtrijnaam, aantal jobs, fouttekst of klantgegevens — en dat kan het ook niet.

De gezondheidssamenvatting wordt met een constant aantal queries per verzoek geproduceerd; het aantal queries groeit niet met het aantal sleutels. Lees voor details de recente verzoeken van die sleutel met lg logs en de uitgaande koppeling met lg webhooks list.

Fouten en exitcodes

Waar een machine op vertakt, is een stabiele string. De tekst in het veld error is bedoeld voor een mens, is gelokaliseerd en verandert zonder aankondiging — een integratie die op berichttekst vertakt, wordt niet ondersteund, en als ze breekt, is dat geen bug.

Twee taxonomieën, twee velden. error_code is de transportlaag (de tabel hierboven) en is waar je eerst op vertakt. error_key is de stabiele bedrijfsregelsleutel binnen failed — INSUFFICIENT_CREDITS, NOT_FOUND, DUPLICATE_CONTACT, INSUFFICIENT_STOCK — gepubliceerd als woordenboek in de catalogus (error_keys) en hieronder. Een commando dat nog geen sleutel afgeeft, geeft error_key: null terug; behandel een sleutel die niet in het woordenboek staat als een generieke fout.

Stabiliteitsbelofte

De duurste vraag in een integratiecontract is “draait de code die ik vandaag schrijf morgen nog?” Eén getal beantwoordt die: contract_version in de catalogus, en de header X-Contract-Version bij elk antwoord. Het beantwoordt precies één vraag — draait eerder geschreven code nog ongewijzigd? Als het getal niet is gestegen: ja. Als het wel is gestegen: nee — iets waarop je vertrouwde, is verwijderd of versmald.

Omdat een aankondiging niets verwijdert: je bestaande code draait de dag erna precies zoals ervoor. Bij een aankondiging de versie verhogen zou twee signalen geven voor één wijziging en je leren het eerste te negeren — waardoor het signaal dat er echt toe doet, verzwakt. De aankondiging reist al mee met de aanroep die je doet: deprecated_at en replaced_by op de catalogusrij, een header X-Deprecated plus een blok deprecated in het antwoord van dat commando, en een stderr-waarschuwing van één regel als je lg gebruikt. Je hoeft niets te doen om te weten dat er iets verdwijnt; het staat al in je log.

Tussen aankondiging en verwijdering liggen minstens twee kalenderkwartalen. Gedurende het hele venster antwoordt het oude commando nog, en waarschuwt het bij elke aanroep. Wanneer een CLI-naam wordt uitgefaseerd, neemt zijn opvolger die naam over als alias: tijdens het venster bereikt de oude naam het oude commando, en op de dag dat het wordt verwijderd, komt dezelfde aanroep bij het nieuwe terecht — zonder dat jij een client-release hoeft uit te brengen.

preview_invoice → get_invoice (aangekondigd op 3 september 2026). get_invoice geeft hetzelfde previewblok terug en voegt de regelitems, de uitsplitsing van de belasting, geregistreerde betalingen, het resterende saldo en adressering via je eigen ERP-sleutel (external_ref) toe. preview_invoice blijft werken, net als lg invoices preview — ze waarschuwen allebei.

Identiteitstabel van de commando's

De tabel hieronder is niet met de hand geschreven: hij wordt telkens wanneer deze pagina wordt geserveerd, gegenereerd uit het live commandoregister. Een commando dat op de server wordt toegevoegd, verschijnt hier vanzelf. De kolom "Poort" vertelt je of een commando om bevestiging vraagt en of het effect ongedaan kan worden gemaakt; de kolom "Rollen" vertelt je welke accounttypes het in hun catalogus zullen vinden. Badges markeren de commando's die een sleutel op managerniveau nodig hebben, de commando's die een bestand aannemen en de commando's die alleen vanuit lg draaien.

De API-naam (list_loads) is bevroren en zal niet veranderen; het is de naam die je in HTTP-aanroepen gebruikt. De CLI-identiteit (loads list) valt onder het versiebeleid: eenmaal aangekondigd verandert ze alleen met een versieverhoging en een migratievenster, waarin de catalogus het oude paar als alias serveert. De beschrijvingen hier komen uit het eigen record van elk commando en zijn ingekort — lees voor de volledige tekst, de parametertypes en de enum-waarden het catalogus-endpoint of voer lg help uit. Commando's die alleen voor affiliates zijn, staan hier niet; een affiliate-account ziet ze in zijn eigen catalogus.

ERP- en CRM-integratie — Identiteit: geen serviceaccounts, maar een echte gebruiker

Een ERP of CRM maakt verbinding met Logistivo als een echte bedrijfsgebruiker met een persoonlijk access token dat in het paneel is aangemaakt. Een synthetisch serviceaccount — zonder bedrijf, vrijgesteld van scoping, zonder iemand die ervoor verantwoordelijk is — wordt nooit aangemaakt.

Dit is architectuur, geen voorkeur. Multi-tenant-isolatie rust volledig op het bedrijf van de gebruiker; voor een identiteit zonder bedrijf wordt de isolatiescope inert, en één verkeerde query met dat token leest gegevens die er niet bij horen. De tweede reden is audit: de gebruikerskolom in het logboek koppelt "wie heeft dit uitgevoerd" aan een persoon; een serviceaccount maakt die kolom betekenisloos en laat een integratie die facturen uitgeeft zonder eigenaar achter. De derde is intrekking: als een token bij een persoon hoort, valt de integratie stil wanneer die persoon vertrekt. Een koppeling zonder eigenaar is de koppeling die jarenlang stilletjes blijft schrijven.

ERP- en CRM-integratie — Versiebeheer en uitfasering

Het catalogusantwoord bevat een integer contract_version; commandorijen bevatten since, deprecated_at en replaced_by. De contractversie stijgt alleen bij brekende wijzigingen.

ERP- en CRM-integratie — Stabiele identifiers: geef je ERP nooit een auto-increment-id als sleutel

Interne auto-increment-id's zijn ondoorzichtig en gebonden aan de tenant: de geldige id van een ander bedrijf geeft jou "niet gevonden", geen data. Maar ondoorzichtig zijn maakt ze nog geen bedrijfssleutels. Wat een ERP in zijn eigen record opslaat, moet een bedrijfssleutel zijn.

ERP- en CRM-integratie — Paginering, filtering en incrementele sync

Lijstcommando's nemen limit aan en geven count terug. Voor een nachtelijke sync verbindt het contract zich aan het volgende; een lijstcommando dat het heeft overgenomen, kondigt cursor en updated_since aan in zijn JSON Schema, en zolang dat niet zo is, behandel je het als een klein venster (maximaal 20 rijen, geen cursor).

ERP- en CRM-integratie — Rate limits

De sleutel voor de limiet is het token, niet het IP-adres: een ERP komt van achter één NAT, en een limiet op IP zou een heel bedrijf als één gebruiker tellen. Twee integraties met twee sleutels eten nooit elkaars quota op — de tweede reden voor "één integratie per sleutel".

ERP- en CRM-integratie — Wat we niet zullen doen

Dit is vaak het nuttigste deel van een contract: als je weet wat er nooit komt, kun je eromheen ontwerpen.

De machineleesbare catalogus

Deze pagina is geschreven voor mensen. Voor een AI-assistent of een geautomatiseerde client bestaat dezelfde informatie als een gestructureerde kopie zonder authenticatie.

GET /api/public/cli/catalog vereist geen authenticatie en geeft de publieke identiteit van de commando's terug: de bevroren API-naam, de CLI-identiteit domain/verb, de beschrijving, de JSON Schema-parameters, de vlaggen read_only, confirmation_needed en irreversible, de credit_cost, en de toegangsvelden key_level, permission, file_params, secret_parameters en cli_only. Het geeft geen persoonsgegevens, geen tenantgegevens en geen voorbeeldrecords terug — alleen wat het oppervlak is.

De juiste manier waarop een externe agent het gebruikt: lees de catalogus, leg in JE EIGEN woorden uit welke taken mogelijk zijn, en verwijs de gebruiker, wanneer uitvoering nodig is, naar het uitvoeren in zijn eigen omgeving met zijn eigen token. De agent vraagt nooit om het token van een gebruiker, genereert het niet en geeft het niet door.

Logistivo koppelen aan een AI-assistent voor aanmelding en vrachtaanvragen is een apart kanaal met een eigen pagina:

De machineleesbare definitie: OpenAPI 3.1 en Postman

De catalogus hierboven is het eigen formaat van Logistivo. Dezelfde informatie wordt ook gepubliceerd in twee industriestandaarden die je tools al begrijpen: een OpenAPI 3.1-definitie en een Postman-collectie. Geen van beide is met de hand geschreven — beide worden bij elk verzoek gegenereerd uit het live commandoregister, dus ze kunnen niet verouderen.

De definitie IS de catalogus, geen tweede werkelijkheid. Elk commando verschijnt als een operatie POST /api/common/commands/; het body-schema is het eigen JSON Schema van het commando (byte voor byte wat de assistent ziet), het antwoordschema is de gedeelde envelop, de poortvlaggen (read_only, confirmation_needed, irreversible en de creditbadge) staan in de extensie x-logistivo, en de zakelijke foutsleutels staan onder x-error-keys. Een uitgefaseerd commando wordt gemarkeerd met deprecated: true en blijft gedurende zijn venster van zes maanden aanroepbaar.

Naast het commando-oppervlak staan ook de REST-endpoints die het integratiecontract dragen in de definitie: 10 lijst-endpoints met incrementele sync (updated_since + cursor + limit) en 7 schrijf-endpoints die je eigen sleutel accepteren (external_ref). Het REST-oppervlak van Logistivo is veel groter dan dat; de rest is MET OPZET weggelaten. Die payloads zijn het eigen contract van het product, en een schema dat op gissingen is gebaseerd, zou je gegenereerde client bij de eerste aanroep breken — en je zou de bug in jouw code zoeken, niet in de onze.

De definitie wordt gegenereerd voor de ROL van degene die erom vraagt: het commando van een andere rol komt nooit in het document, omdat een definitie een contract is en geen woordenboek, en alles erin met dat token aanroepbaar moet zijn. De versie zonder authenticatie is de vereniging van de drie rollen, en daar vermeldt elke operatie onder x-logistivo.roles welke rollen haar zien.

Geloof ons niet op ons woord — controleer het zelf: npx @redocly/cli lint logistivo-openapi.json slaagt met nul waarschuwingen.

Codegeneratie: een getypeerde client uit de definitie

Je hebt de definitie; genereer er nu werkende code uit. `lg codegen` leest het OpenAPI-document en schrijft een client van één bestand zonder afhankelijkheden — TypeScript (typedeclaraties plus een dunne wrapper voor aanroepen) of PHP (een clientklasse met PHPDoc-array shapes). Geen `npm install`, geen composer: `fetch` aan de TypeScript-kant, `ext-curl` aan de PHP-kant.

De generatie gebeurt op JOUW machine, niet op onze server. Dat is geen voorkeur: pin de definitie één keer vast in je repository (`lg spec --out=...`) en hetzelfde commando draait bij elke build zonder netwerk en zonder token, zodat je CI nooit afhankelijk is van onze uptime. En omdat de gegenereerde code code is die in je ERP DRAAIT, zorgt het bewaren ervan in je repository, in plaats van het ophalen via een API, ervoor dat elke wijziging zichtbaar wordt in `git diff`.

De gegenereerde client behandelt alle drie Logistivo-sporen correct, en hij leert welk spoor welk is uit het document in plaats van te gokken: elke operatie declareert haar spoor in `x-logistivo.surface`. Het commandospoor pakt de envelop `{success, data:{…}}` uit; een REST-synclijst heeft GEEN envelop en wordt teruggegeven zoals hij is (uitpakken zou `next_cursor` laten wegvallen); een REST-schrijfactie geeft de eigen productbody van het endpoint terug. Een generator voor algemeen gebruik kent dit onderscheid niet — de onze wel.

De types zijn geen giswerk: de argumentinterface van elk commando wordt gegenereerd uit precies het JSON Schema dat de assistent ziet. Verplicht blijft verplicht, enums worden union types, geneste objecten worden geneste types. In de praktijk: als de server een veld vereist, faalt de aanroep die het vergeet niet met een 422 in productie — hij faalt bij het compileren.

Er wordt geen generatietijdstempel in het bestand geschreven: dezelfde definitie met dezelfde vlaggen levert dezelfde bytes op. Opnieuw genereren maakt daardoor geen ruis in `git diff`, en het enige dat echt veranderd is — het contract — blijft zichtbaar.

Eerlijke afbakening

We breiden deze sectie liever uit dan dat we haar inkrimpen. De waarde van een integratiecontract zit minder in wat het belooft dan in hoe duidelijk het zegt wat het niet doet.

Veelgestelde vragen

Waarvoor wordt de Logistivo CLI gebruikt?

Hij voert het werk in je Logistivo-account uit vanuit een terminal, een script of je eigen AI-agent: ladingen en ritten, aanvragen, biedingen en tenders, wagenpark, chauffeurs en hun onkosten, documenten, contacten en rekeningen-courant, facturen en boekhouding, magazijn en voorraad, teamleden, HS-tarieven en antidumpingrechten. Elke schrijfactie in het paneel heeft een commando; betalingen, juridische toestemming en het sluiten van een account blijven bewust in de browser. Dezelfde commando's zijn via HTTP aan te roepen voor ERP/CRM-integraties en via het MCP-kanaal.

Hoe worden commando's in de Logistivo CLI benoemd?

De vorm is lg --parameter=value. Het domein is een entiteit in het meervoud (loads, demands, invoices, export-documents) en het werkwoord komt uit een gesloten woordenschat die alleen door een bewuste beslissing groeit (list, get, create, update, search, set-status, generate enzovoort). Alle werkwoorden van één entiteit wonen in één domein, en het paar (domein, werkwoord) is uniek in de hele catalogus.

Hoe authenticeer ik me bij de CLI?

Interactief toont lg login een goedkeuringscode die je in je browser bevestigt nadat je bij Logistivo bent ingelogd — er wordt geen wachtwoord of sleutel in de terminal getypt, en de sessie blijft ingelogd en vernieuwt zichzelf totdat je lg logout uitvoert. lg login --password (e-mailadres en wachtwoord) blijft beschikbaar als terugvaloptie. Gebruik op servers en in CI een integratiesleutel die is uitgegeven in het paneel of met lg keys create: lg login --api-key slaat hem op in een lokaal profiel, of je zet hem in de omgevingsvariabele LOGISTIVO_TOKEN. Via HTTP reist het token mee in de header Authorization: Bearer.

Kan ik een serviceaccount aanmaken voor mijn ERP-integratie?

Nee. Een integratie maakt verbinding met het token van een echte bedrijfsgebruiker die in het paneel is aangemaakt. Een synthetische identiteit zonder bedrijf maakt multi-tenant-isolatie inert, maakt de kolom "wie heeft dit uitgevoerd" in het auditlogboek betekenisloos en laat een koppeling achter die niet stilvalt wanneer haar eigenaar vertrekt. Het juiste patroon is een speciale gebruiker met een beperkte rol en een met naam genoemde eigenaar.

Heeft Logistivo een testomgeving (sandbox)?

Ja, en die is selfservice: er wordt een aparte sandbox-tenant voor je bedrijf geopend — dezelfde code, dezelfde commandocatalogus, een eigen creditadministratie. De omgeving wordt afgeleid van het bedrijf van de sleutel en kan niet met het verzoek worden meegegeven; een aanroep met het verkeerde profiel stopt aan de serverkant met 403 ENVIRONMENT_MISMATCH. Een aanvraag die je in de sandbox opent, bereikt nooit echte vervoerders, een sandbox-event wordt nooit aan je live webhookabonnement afgeleverd, en je live saldo beweegt niet. Endpoints: POST /api/common/integration-sandbox (openen), POST /api/common/integration-sandbox/keys (sleutel), GET /api/common/integration-sandbox/go-live (go-live-checklist). Aan de kant van lg zijn lg --sandbox en lg --live profielkiezers voor één run.

Hoe lees ik de uitvoer van lg --json in een script?

De uitvoer is de hele HTTP-envelop: { "success": bool, "data": { "contract_version", "command", "status", "result" }, "code", "message" }. De zakelijke payload staat onder .data.result; bij lijstcommando's zijn de rijen .data.result., is het aantal .data.result.count en is de volgende pagina .data.result.next_cursor. Het juiste jq-pad ziet er dus uit als lg loads list --json | jq -r '.data.result.loads[].load_code'. Een pad dat de envelop overslaat, geeft geen fout, het geeft stilzwijgend null terug — in een watchdog-script wordt dat een alarm dat nooit ophoudt af te gaan. De enige uitzondering is /api/public/status, waarvan de body bewust envelopvrij is.

Als ik hetzelfde verzoek twee keer stuur, krijg ik dan dubbele records?

Niet als je een idempotency_key hebt meegestuurd. Een tweede aanroep met dezelfde sleutel voert het commando niet opnieuw uit; hij geeft het eerste resultaat terug met status replayed. De sleutel moet een UUID zijn en bij retries DEZELFDE blijven — per poging een nieuwe sleutel genereren is hetzelfde als geen idempotency gebruiken.

Wat doet --yes?

Het passeert de bevestigingspoort. Wanneer een bevestigingsplichtig commando zonder --yes wordt aangeroepen, verandert er niets: de server vat in één zin samen wat hij gaat doen, zegt of het onomkeerbaar is, en stopt (HTTP 409, CLI-exitcode 4). Het commando herhalen met --yes voert het werk uit.

Hoe detecteer ik een mislukt commando in een script?

Lees de exitcode, nooit de berichttekst: 0 succes, 1 afwijzing door een bedrijfsregel, 2 commando- of argumentfout, 3 authenticatie- of rolfout, 4 bevestiging vereist. Via HTTP zijn de equivalenten 200, 422, 404, 403 en 409, en het veld error_code in de body is stabiel. Het fout-BERICHT is gelokaliseerd en verandert zonder aankondiging.

Hoe ontdek ik de beschikbare commando's programmatisch?

GET /api/public/cli/catalog voor discovery zonder authenticatie, en GET /api/common/commands voor de lijst die je account daadwerkelijk kan zien. Beide geven per commando de JSON Schema-parameters terug; bouw je client daaruit op in plaats van een commandolijst hard te coderen.

Moet ik mijn client bijwerken wanneer er een nieuw commando wordt toegevoegd?

Nee. De client haalt zijn commandolijst uit de catalogus, dus een commando dat op de server wordt toegevoegd, verschijnt zonder nieuwe client-release. Een update is alleen nodig bij een brekende wijziging die de contractversie verhoogt, en in dat geval blijft de oude vorm minstens zes maanden in leven.

Vervangt de CLI de bestaande REST API?

Nee. Het REST-oppervlak dat de web- en mobiele apps aandrijft, blijft bestaan. De commandocatalogus vervangt resource-CRUD niet; hij is de laag van de werkwoorden — de enige manier om een actie uit te voeren met rolcontrole, bevestigingspoort en audit.

Hoe krijg ik een Logistivo-API-sleutel en wat kan die?

Een manager met het recht voor teambeheer geeft hem uit in het paneel onder Settings › Integration keys, of vanuit een terminal met lg keys create. De sleutel handelt namens een echt teamlid en heeft drie onafhankelijke instellingen: scope (read opent de catalogus en niet-schrijvende commando's, write opent elk commando — de bevestigingspoort blijft zoals ze is), optionele domeinrechten, en een toegangsniveau — employee, de standaard, voor dagelijkse operaties, of manager voor team, instellingen, sleutels en geld. Hij wordt één keer getoond en niet op de server opgeslagen; hij kan worden geroteerd, overgedragen en ingetrokken. Wanneer het lid namens wie hij handelt vertrekt, wordt ook de sleutel gesloten. Er is geen serviceaccount.

Wat gebeurt er als ik een idempotency key hergebruik voor een ander commando?

De server antwoordt 409 idempotency_conflict en noemt het commando waarvoor de sleutel eerst werd gebruikt; er draait niets en er wordt niets herhaald. Sleutels zijn beperkt tot je bedrijf, gebonden aan één commando en 24 uur vergrendeld — daarna start dezelfde sleutel een nieuwe uitvoering.

Hoe worden rate limits op mijn integratie toegepast?

Per token, per minuut, in drie buckets: catalogus lezen 120, leescommando's 120, schrijvende commando's 30. Een 429 bevat altijd Retry-After en een body met retry_after, limit en bucket; wacht zo lang en probeer het opnieuw. De catalogus publiceert de actuele getallen onder rate_limits.

Hoe onderscheid ik een zakelijke afwijzing van een transportfout?

Vertak eerst op error_code: failed (422) is een zakelijke afwijzing en bevat een stabiele error_key zoals INSUFFICIENT_CREDITS of NOT_FOUND; server_error (5xx) en netwerkfouten worden opnieuw geprobeerd met dezelfde idempotency key; rate_limited (429) wacht je af. Vertak nooit op de fouttekst.

Verstuurt Logistivo webhooks? Voor welke events?

Ja. Je registreert een https-adres en kiest de events die je belangrijk vindt; er komt een ondertekende POST binnen wanneer ze gebeuren. Vandaag kun je je op 13 events abonneren: lading aangemaakt/status gewijzigd/chauffeur toegewezen, aanvraag aangemaakt en status gewijzigd, bod uitgebracht/herzien/geaccepteerd, factuur concept/uitgegeven/geannuleerd, en magazijnreservering aangemaakt/status gewijzigd. Elke naam wordt gedekt door een eventklasse die daadwerkelijk wordt verzonden — niets wordt aangekondigd en daarna nooit afgevuurd, want zo'n event is een stille fout waar de integrator eeuwig op wacht. Abonnementen worden aangemaakt onder Settings › Integration › Webhooks, of vanuit een terminal met lg webhooks create.

Hoe verifieer ik de handtekening van een Logistivo-webhook?

Elk verzoek bevat een header X-Logistivo-Signature: t=,v1=. De ondertekende string is ".", het algoritme is HMAC-SHA256 en de sleutel is het secret van het abonnement. Vier regels: (1) verifieer de RUWE bytes — de body decoderen en opnieuw serialiseren (JSON.stringify / json_encode) maakt van dezelfde data andere bytes en dan klopt de handtekening niet meer; dit is verreweg de meest gemaakte fout; (2) controleer de tijdstempel en laat alles vallen waarbij |now − t| > 300 s, ook als de handtekening klopt; (3) vergelijk in constante tijd met hash_equals / timingSafeEqual; (4) ontdubbel op event_id, niet op de aflever-id — event_id blijft behouden bij een replay, terwijl de aflever-id verandert. Werkende ontvangers voor Node en PHP staan in examples/webhook-receiver/ in de repository en testen zichzelf met --selftest.

Wat gebeurt er als een webhook niet kan worden afgeleverd? Verlies ik het event?

Nee. Als je ontvanger geen 2xx teruggeeft, komt de aflevering in het retryschema: 60 · 300 · 1800 · 7200 · 21600 seconden, 6 pogingen in totaal, een venster van ongeveer 8,6 uur. ELKE fout behalve 410 Gone wordt opnieuw geprobeerd, 4xx inbegrepen — een zakelijk event verliezen kost meer dan een paar extra verzoeken. Na 10 opeenvolgende uitgeputte afleveringen wordt het abonnement stilgelegd en wordt de reden vastgelegd, zodat "waarom komt er niets binnen" beantwoordbaar blijft. Het afleveringslogboek bewaart elke poging (statuscode, duur, fout) en je kunt één aflevering opnieuw versturen vanuit het paneel of met lg webhooks replay. Incrementele sync (updated_since + cursor) is er nog steeds als vangnet.

Kan mijn AI-agent alles doen wat een gebruiker in het paneel kan doen?

Bijna alles. Elke schrijfactie in het paneel heeft een commando — gemeten tegen de schrijf-endpoints van het paneel is 100% van de endpoints binnen de scope gedekt — en dezelfde commando's staan open voor lg, voor HTTP en voor het MCP-kanaal. De bewuste uitzonderingen zijn stappen rond identiteit en geld: inloggen, een pakket kopen, een kaart toevoegen met 3D Secure, juridische teksten goedkeuren en een account sluiten blijven in de browser van de persoon, en lg handoff-links generate geeft daarvoor de link terug. Integratiesleutels, webhooks en de sandbox worden beheerd met ingebouwde lg-commando's, omdat ze geheimen één keer teruggeven.

Kan ik bestanden zoals pdf's of foto's vanuit de CLI of de API aan commando's doorgeven?

Ja. Zet bij lg een @ voor een lokaal pad (--attachment_uuid=@./cmr.pdf) en lg uploadt het bestand eerst; --file is een snelkoppeling wanneer een commando één bestandsveld heeft, en lg upload uploadt alleen. Via HTTP stuur je het bestand als multipart naar POST /api/common/commands:upload en geef je de teruggegeven attachment_uuid door aan het commando; MCP-clients gebruiken de tool upload_attachment. Het type wordt uit de inhoud bepaald, uploads horen bij de gebruiker en blijven 24 uur bruikbaar, en hetzelfde bestand opnieuw uploaden geeft dezelfde id terug.

Wat is het verschil tussen een integratiesleutel op employee- en op managerniveau?

Het toegangsniveau bepaalt hoe zwaar het werk mag zijn dat een sleutel doet. Een employee-sleutel, de standaard voor nieuwe sleutels, voert dagelijkse operaties uit: ladingen, aanvragen, biedingen, ritten, wagenpark, documenten en contacten. Een managersleutel mag ook beheerwerk doen — teamleden en rollen, bedrijfsinstellingen, integratiesleutels, bankrekeningen, opgeslagen kaarten, affiliate-uitbetalingen. Een sleutel heeft nooit meer rechten dan de persoon namens wie hij handelt, en een employee-sleutel die een managercommando aanroept, krijgt INSUFFICIENT_KEY_LEVEL. Wijzig het niveau in het paneel of met lg keys update --access-level=manager.

Kan een affiliate (doorverwijspartner) de CLI gebruiken?

Ja. Een affiliate-account krijgt een eigen catalogus — 55 commando's voor doorverwijzingen, leads, CRM-notities en -taken, salespresentaties, verdiensten en uitbetalingen — en gebruikt die vanuit lg, HTTP of MCP, net als elke andere rol. Een uitbetaling aanvragen of annuleren en de bankrekening voor uitbetalingen wijzigen vereisen een sleutel op managerniveau en een bevestiging. Een bedrijf dat ook verlader, vervoerder of douane-expediteur is, ziet beide catalogi.

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.