Logistivo CLI: run your logistics operation from a terminal and from your ERP
One command catalog feeds three surfaces: the lg terminal client, the AI assistant inside the dashboard, and the channel external AI assistants connect to. A capability is written once and appears in all three. This page is a contract. The command table below is not hand-written; it is generated from the live command registry on every request.
What is the Logistivo CLI?
The Logistivo CLI (lg) is a command line client that runs the freight, demand, bid, export-document, stock, invoice and customs-tariff operations of your Logistivo account from a terminal or a script. The commands are not baked into the client: the client pulls the command catalog from the server and draws itself from it. The same commands can be called over HTTP without installing anything — that is the path ERP and CRM integrations use.
One command shape: lg --parameter=value. The catalog holds 207 commands across 61 domains today; the widest list a single role sees is 185 commands across 59 domains.
Flags are never invented: every flag is a parameter name from the command's own JSON Schema, and so are its type and whether it is required.
Writing commands ask for confirmation; irreversible ones never run without it (HTTP 409, CLI exit code 4).
Retrying does not create duplicates: a call carrying an idempotency_key runs at most once.
Every execution lands in one audit ledger — whether it came from a terminal, from the chat, or from an assistant is on the record.
What is live today, and what is on the way
This section sits at the top of the page on purpose. The most expensive mistake an integration document can make is to describe an endpoint that is not open yet as if it were: the integrator writes the code, the endpoint returns 404, and trust does not come back.
Because the contract is frozen, the integration code you write today keeps working as the remaining pieces open. Fields you do not know must be ignored; that rule is what lets us add without breaking.
Command catalog and identity table — The public domain/verb identity of every command is frozen and the contract version is published as 1 in every catalog response and in the X-Contract-Version header. The table on this page is generated from the live registry: a command added on the server appears here on its own, and a command removed disappears.
Unauthenticated discovery endpoint — GET /api/public/cli/catalog — command names, domain/verb identity, description, JSON Schema parameters, gate flags and the versioning fields (since, deprecated_at, replaced_by, aliases). No authentication: minting a token is a step that comes AFTER the integration decision, not before it. The response carries no tenant data at all.
Authenticated catalog and execution endpoints — GET /api/common/commands, GET /api/common/commands/{name} and POST /api/common/commands/{name}. The role gate, the confirmation gate, idempotency and the audit ledger live at a single enforcement point that the chat and the terminal pass through as well.
Preflight (dry_run): see it without running it — With dry_run: true in the request body the command does not run: validation, the role/scope gate, target resolution and the credit calculation all execute and a preview comes back; nothing is written, no credit is spent and the idempotency key is not consumed. The batch endpoint takes it too, per batch. Measured across every write command: not one write statement in 178 preflights.
Integration keys — API keys issued from the panel that act on behalf of a real team member: read/write scope, lifetime, last use, rotate and revoke. lg login --api-key, lg whoami and a server-side-revoking lg logout are built on them.
Sandbox — rehearse writes without touching production — A separate sandbox tenant per company with the same command catalog; opened self-service and it issues its own key. Credit, the marketplace and webhook routing are split between the two environments; the environment is derived from the key's company and cannot be supplied with the request.
Versioning, error taxonomy, idempotency and rate limits — Commands carry since / deprecated_at / replaced_by / aliases and a deprecated command answers with an X-Deprecated header. Business failures carry a stable error_key next to the transport-level error_code, and the key dictionary is published in the catalog. Idempotency keys are scoped to your company, bound to one command and locked for 24 hours. Rate limits are per token, in three buckets, and every 429 carries Retry-After.
Outgoing webhooks: a POST when something happens — Signed push is live for 13 events. Subscriptions are managed from the panel or with lg webhooks; the signature carries its own timestamp, the retry schedule is 60 · 300 · 1800 · 7200 · 21600 seconds and the muting reason is always recorded. Every event name is backed by an event class that is actually dispatched — nothing is announced and then never sent. The pull rail (updated_since + cursor) was not removed: push was added next to it, not instead of it.
The lg terminal client — The single-file, dependency-free client is in early access. Because the HTTP surface is open you can integrate without waiting for it; lg is only a convenience placed in front of the same endpoints. It already honours Retry-After, retries 5xx with the same idempotency key and warns on deprecated commands and newer contract versions.
Incremental sync: cursor pagination and updated_since — The contract below (limit + opaque signed cursor, next_cursor, updated_since) is fixed and is being rolled out across list commands. Until a given list command advertises cursor and updated_since in its JSON Schema, treat it as a small window: ceiling 20 rows, no cursor.
What makes this different: the client knows no commands, it draws them
This is the single decision that makes the rest of the page meaningful. In an ordinary CLI the commands live inside the client; in Logistivo they live on the server.
In the classic arrangement, when the server gains a capability the client needs a new release and the user needs to install it. In between, the two sides know different things — and that is the quietest failure mode in integrations: the script runs, the exit code is 0, and the command simply does not describe what the server can do.
In Logistivo a command's definition is a single record: its name, what it does, its JSON Schema parameters, which roles may see it, whether it needs confirmation and whether its effect is reversible. When lg starts it pulls that catalog and generates its help text, its flags and its input validation from it. The moment a command is added on the server, lg knows it — with no client update.
The same catalog feeds the AI assistant in the dashboard and the channel external AI assistants connect to. A capability is written once and appears in three places — and all three pass through the same role gate, the same confirmation gate and the same audit ledger. There is no separate CLI command inventory; if there were, the three surfaces would quietly diverge within six months.
No version drift — The command list the client knows is, by construction, the list the server has. "Which lg version has this command?" is not a question that exists.
Flag names are never guessed — A command's flags are exactly the parameter names in its JSON Schema. The way to learn what a command accepts is not to read a document but to read the catalog — and the catalog is always current.
Your CLI is as large as your account — The catalog is role-filtered: a command your role cannot see never appears in your list. A carrier account sees 181 commands, a shipper 185, a customs broker 122.
The documentation cannot fall behind — The command table on this page comes from the same registry. There is no list anywhere that a human has to keep in sync with reality.
The naming rule — how command names grow
The catalog will grow. New commands are not named ad hoc; six rules are applied. We publish them here because these are the names your integration will be writing for years, and their predictability is worth something to you.
A domain is a plural entity — loads, demands, invoices, export-documents. Two-word domains are kebab-case. The single exception is an uncountable noun: stock is never pluralised, because "stocks" means something else.
Every verb of one entity lives in one domain — loads carries six verbs: create, list, get, list-problems, set-status, assign-driver. Opening a new domain per verb (load-statuses, load-drivers) would make a user hunt for the same entity in three places.
The verb never leaks into the domain; the domain is what CHANGES — Assigning a driver changes the load, not the driver, so it is lg loads assign-driver. The indirect object (-driver) is attached to the verb, never to the domain.
A sub-resource never gets its own domain — An invoice line and a document item cannot be listed on their own; they are addressed through their owner — lg invoices add-line, lg export-documents add-item. One test decides it: if you cannot list or get the thing independently, it is not a domain.
Narrowing is a flag, not a new verb — lg fleet-documents list --days=30 is correct; there is no separate "list-expiring" verb, because the window is already a parameter. A qualified verb is used only when the rows it returns are not the domain entity: loads list-problems returns problem flags, not loads.
The (domain, verb) pair is unique across the whole catalog — Even for two commands whose roles never overlap. If the same pair were given to two commands, your integration code would be looking at one name while the CLI ran different things depending on who invoked it.
Install and identity
lg is a single-file, dependency-free Node script (Node 18+). It was written that way so it can be copied onto a customer's ERP server as one file: a client that needs npm install never gets there. The client is in early access and the download link will be added to this section when it opens. You do not have to wait for it to build an ERP/CRM integration — the same commands are available over HTTP.
1. Log in — lg login asks for your email and password, writes the returned access token to ~/.logistivo/config.json and fetches the catalog straight away. If email verification is enabled on your account it asks for the code here too — skipping that step would leave you holding a token while every protected endpoint refuses it.
2. For servers and CI: a key, not a password — Non-interactive environments do not type a password into lg login. Issue a key in the panel (Settings › Integration keys) — optionally on behalf of a dedicated, narrowly-scoped team member — and put it in LOGISTIVO_TOKEN, or save it with lg login --api-key. While the variable is set the config file is never read. Details: the "Connecting with an API key" section.
3. Verify and explore the catalog — lg commands lists every command your role can see, grouped by domain; lg help prints that domain's verbs, and lg help prints one command's full parameter list with types and which ones are required. None of that text lives inside the client — all of it comes from the catalog. Remember that the list depends on your role: the verbs of one domain are not the same for a carrier and for a shipper (loads set-status, for instance, is in the carrier catalog only). If a verb is missing, check your role before you check your install.
Never put the key into version control, a CI log or a chat window. If you signed in with an integration key, lg logout revokes it on the server too; in a password session it only deletes the LOCAL copy and says so explicitly. The audit ledger stores your arguments but masks any field whose name contains token, password, IBAN, card or OTP as ***.
LOGISTIVO_TOKEN — Personal access token. While set, the config file is never read — this is the preferred path in CI and on servers.
LOGISTIVO_BASE_URL — Server root. Defaults to https://logistivo.com. Change it only if you are pointing at your own environment; the --base-url flag does the same for a single command.
LOGISTIVO_HOME — Directory for the config file and the catalog cache. Defaults to ~/.logistivo. Separate it if you work with two accounts on one machine.
LOGISTIVO_PROFILE — Which profile to run as. `lg profile use` is the persistent choice; this variable is for a single run and is the shortest way to switch between a live and a sandbox profile in CI. An unknown profile name is not silently ignored — it errors.
NO_COLOR — When set, no ANSI colour is printed (the permanent form of the `--no-color` flag). Turn it on for a bridge that writes into a log file or CI output; colour escape sequences break the tools that read those logs later.
Client ergonomics
These features make day-to-day use of the `lg` client easier. None of them change the HTTP contract — skip this section if you integrate over HTTP directly.
Multiple environments and keys — Keep staging and production, or your customer and carrier bridges, in separate profiles. The profile list never prints a token; each row shows the token's sha256 fingerprint. The command catalog is cached per profile, host and token, so two roles can never see each other's command list.
Incremental sync: lg sync — Runs the cookbook sync loop in one command: a per-entity watermark, updated_since with a cursor, JSONL output and a five-minute safety margin. Use --full for the first backfill. The command is read-only; there is no --write flag. lg sync --list derives the syncable entities for your role from the catalog.
Error experience and shell completion — On 422 the client names the rejected field as the flag you typed, and the stable error_key together with the catalog's own explanation of it. On 409 it prints the ready --yes command line; on 429 it waits out Retry-After. Shell completion covers domains, verbs, flags and enum values, and works offline.
Connecting with an API key
An ERP, CRM or script connects to Logistivo with an integration key issued from the panel. The key acts on behalf of a real team member: that person's role decides the command list, and when that person leaves the team the key dies with them. There is no synthetic "service account" and there will not be one — multi-tenant isolation is bound to the user, and an ownerless bridge is the kind that silently keeps writing for years.
1. Issue a key in the panel — Settings › Integration keys › New key. Give it a name ("ERP nightly sync"), pick the scope and optionally shorten the lifetime. The key is shown once, on that screen only; the server does not store it. If you lose it, Rotate issues a new one — the old one becomes invalid immediately.
2. Connect from the terminal — lg login --api-key validates the key, prints the identity and scope, and downloads the command catalog. lg whoami asks the server which user, company, role and scope you are running as. lg logout revokes the key on the server as well (the password-session lg logout cannot do that).
3. Or plain HTTP — The key is an access token carried in the Authorization: Bearer header and works directly against the three command endpoints. On a server or in CI put it in LOGISTIVO_TOKEN; while that variable is set the config file is never read.
Scope is enforced across the whole API surface, not only the command catalog: a read-only key passes read requests (GET/HEAD/OPTIONS) and every other writing request answers 403 forbidden. There are two deliberate exceptions and both are guarded by a finer gate: the command execution endpoint (the decision comes from the command's own read_only flag, because non-writing commands are also called with POST) and a key revoking itself (lg logout, allowed on a read-only key too). Still, issue the key on behalf of the least-privileged member that covers your needs: scope limits what you can write, the role decides which records you can see at all.
read — The command catalog and non-writing commands (list, search, preview). A writing command returns 403 forbidden — before the confirmation gate; a read-only key never even sees a 409. The right choice for nightly sync and reporting.
write — Every command. Commands that require confirmation still ask for it with 409; confirm: true (--yes in the CLI) works exactly as before. Scope never loosens the confirmation gate.
One key per integration — If CRM and ERP share a key, cutting one means cutting the other. Give every bridge its own key; the list shows them by name anyway.
Rotate — Issues a new token and revokes the old one immediately; the remaining lifetime is kept. Use it on suspicion of a leak and at regular intervals. The integration gets 401 until it is updated with the new token.
Revoke — The key is closed permanently; it cannot be undone, issue a new key if needed. If the member the key acts for is removed from the team or deactivated, the key drops on its own.
Lifetime and limit — Default validity is 180 days; you can shorten it at creation, never extend it. A user can hold at most 10 active keys. Last-use time is visible in the list; a never-used key says so explicitly.
Who manages — Issuing, rotating and revoking is open to managers with team-management permission — the same gate as adding or removing team members. A manager can issue a key on behalf of another member; a dedicated, narrowly-scoped member is recommended for a bridge.
The first ten minutes: from the panel to your first call
The most expensive minutes of an integration are the first ten. The four steps below lay out that path in order; the same order exists in the panel as a wizard (Settings › Integration › Setup wizard), and the wizard calls exactly the endpoints described here. A developer without access to the wizard reaches the same place by reading this section.
None of the four steps ends with "I sent it". Whether the key really works is answered by the request ledger; whether the webhook really arrives is answered by the delivery ledger. Both are open in the panel and in lg. A setup that is never measured is a setup that is merely believed to work.
1. Decide what you will do — set the scope first — There is no deciding later what a key may see: you choose the scope when you issue it. There are two axes. The coarse scope (read / write) only answers "may it write". The fine scope (domain grants: loads:read, invoices:write) says which domains it may touch, and the moment it is set the key becomes fail-closed — every domain you did not select, and every endpoint that maps to no domain, returns 403. The domain list in the panel is not a hand-written list; it is derived from your role's live command catalogue and shows how many commands and REST endpoints each domain opens. The command catalogue, "who am I" (current) and "log out" endpoints are always exempt from scope: a key must always be able to describe and close itself.
2. Issue the key — the token is shown once — Settings › Integration › New key. Give it a name ("ERP nightly sync"), pick the user it runs on behalf of and shorten the lifetime if you want. The raw token is returned only in that response; the server does not store it and there is no "show again" in the panel. If you lose it, rotate to get a new one and the old one dies instantly. Never put the token in version control, a CI log or a chat window; put it in the target system's secret store.
3. Make the first call — and see that it REALLY arrived — Two requests are enough. The first proves your identity (it is scope-exempt and works with every key): is the scope what you expected, is the status active, is expires_at approaching. The second reads real data from the domain you picked. If it passes, identity, scope, the e-mail gate and the network path are all correct. If it does not, do not guess: the key's own request ledger shows, row by row, which endpoint was rejected with which code and which stable error key (INSUFFICIENT_SCOPE, IP_NOT_ALLOWED, KEY_ACTOR_BLOCKED …). In the panel that ledger is the "Recent requests" window; in the terminal it is lg logs. The wizard watches exactly this ledger and refuses to turn the step green until it sees a real successful call.
4. Set up the webhook — and verify the signature before doing anything — Waiting for events is cheaper than asking every minute. Register an https address, pick the events you care about and bind the subscription to your key: if the key is revoked, delivery stops too. The secret is shown only once, at creation. Your receiving endpoint must verify the signature first: the signed string is "{t}.{raw body}" and the algorithm is HMAC-SHA256; the timestamp is INSIDE the signature, so drop any request older than 300 seconds even when the signature is valid. Compare in constant time and never re-serialise the body — the moment you parse the JSON and print it again the signature stops matching. From the panel, "Send test" fires a real signed ping and you can watch the delivery attempt by attempt.
Sandbox: practise writing without touching production
Preflight (dry_run) tells you what a command WOULD do; but whether idempotency really replays, whether the webhook really gets delivered, what the error branches look like and where the credit comes from can only be seen in a REAL write. That is what the sandbox is for: a separate tenant that mirrors your company — same code, same command catalog — and never touches your live ledger.
The sandbox is not a request flag, it is a TENANT. A separate Logistivo company opened for yours: its own user, its own package, its own credit ledger, its own webhook subscriptions. The environment is DERIVED from the key's company and cannot be supplied by the caller, so even if you pick the wrong profile the request stops server-side with 403 ENVIRONMENT_MISMATCH. "I thought I was writing to the sandbox and wrote to production" is structurally impossible.
The surface is identical — measured: the catalog a sandbox key draws is the same one your live key draws (on a shipper key, 185 commands on both sides). A bridge that works in the sandbox works in production; nothing you learn has to be learned twice.
The sandbox does not open empty: it contains generated (NOT copied) seed data — contacts, loads in both directions, and demands. The names are openly invented and the e-mail addresses sit in the @sandbox.invalid domain; no real tenant's row is ever read, cloned or used as a template.
There is deliberately NO reset button, and no record, file or document is ever deleted. If you want a clean start, renewal opens a new generation and the old one stays, retired. A few hundred accumulated rehearsal rows cost nothing next to a deletion routine wired to the wrong tenant.
1. Open the sandbox — One sandbox per company. The endpoint requires team-management permission (team.manage), so an administrator in your company does this. If it already exists, the same endpoint answers over GET with its state: company id, remaining credit, active key count, subscriptions, generation history and the ceilings.
2. Issue a sandbox key and keep it as a second profile — The key is issued on behalf of the sandbox's own bridge user and comes back stamped environment: sandbox; the raw token appears only in that one response. On the lg side do not replace your live key — keep BOTH as separate profiles; --sandbox and --live are single-run selectors.
3. Actually write — No --dry-run here; writing is the whole point. Send the same idempotency key twice and watch the second response come back as status: replayed — that is the measured result in the sandbox. Call a gated command without confirmation on purpose and read the 409 body; try a command outside your scope and read the refusal. None of it leaves a trace in your live ledger: your live balance is unchanged across the same run.
4. Read the checklist before you go live — The go-live endpoint answers from what you ACTUALLY did in the sandbox: did you write, did you see the error branches, was a webhook delivered, did you move the receiver off the rehearsal address, did you put an expiry on the key. Each item comes back as pass / todo / unknown. The same response also carries a SCOPE PROPOSAL derived from the commands you actually called — use it to issue your live key no wider than you need.
Credit and invoicing — The sandbox has its own credit ledger and your live balance does not move. That is a structure, not a check: credit is counted per company and the sandbox is a separate company. It opens on the free tier, has no payment method and sits outside the monthly collection window — it cannot reach an invoice by any path.
Marketplace — A demand you open in the sandbox does NOT appear on real carriers' screens and does NOT reach their e-mail or push inbox; and you do not see real demands either. The split is symmetric and enforced in two separate queries: who a demand is VISIBLE to and who it is SENT to are different questions.
Webhooks — Subscriptions carry the environment stamp too. A sandbox event is never delivered to a live subscription and a live event never reaches a sandbox one; for a subscription whose stamp has drifted no delivery is opened at all. Because the subscription ceiling is per company, your rehearsal subscriptions do not eat into your live quota.
Seed data — The sample records inside the sandbox are GENERATED, never copied from real tenants. Seeding is idempotent: a second call does not lay a second seed on top of your own records.
The same catalog — Your role is your role in the sandbox too, and the command list is identical — measured, 185 commands on both sides. The sandbox is not a cut-down demo; it is a twin of your company.
Today's limit: webhook subscriptions cannot be managed with a sandbox key — The subscription endpoints require the team-management permission (team.manage), and the sandbox's bridge user carries no team role, so `lg --sandbox webhooks …` answers 403 today. The whole command surface works — the restriction is only on the subscription ledger. If you want to rehearse webhooks in the sandbox, ask support; this is measured and recorded as an open gap.
GET /api/common/integration-sandbox — State: whether it exists, which company, remaining credit, active key and subscription counts, generation history, the ceilings, and a trigger guide showing which command raises which event.
POST /api/common/integration-sandbox — Opens the sandbox (one per company, idempotent). Requires the team.manage permission.
POST /api/common/integration-sandbox/keys — 201 + the key and the raw token (only here, only once). The environment: sandbox stamp is derived from the company; the caller cannot supply it.
GET /api/common/integration-sandbox/go-live — The go-live checklist and a scope proposal — computed from your real traffic in the sandbox.
POST /api/common/integration-sandbox/renew — Opens a new sandbox GENERATION and retires the previous one.
The command model
There is one grammar and it has no exceptions. To know how to call a command, the only thing you need is what the catalog says about it.
lg loads list --status=in_transport --limit=20 --json
Flag names are schema names and may contain underscores (--load_code, --amount_per_vehicle). That is deliberate: seeing a flag tells you which JSON field it lands in, so moving from the CLI to HTTP requires no name translation. A boolean parameter written without a value is true (--only_open); the opposite is --no-only_open. Write an argument whose value starts with a dash as --flag=value, otherwise a forgotten flag silently swallows the next one as its value.
lg — The client.
loads — The domain — the entity you are working on, plural.
list — The verb — drawn from a closed vocabulary (list, get, create, update, delete, search, preview, set, issue, generate, extract, inquiry, move, adjust, and compounds prefixed add-, update-, remove-, set-, assign-).
--status=in_transport — A parameter — its name is exactly the JSON Schema parameter name and its value must match the schema type. An invalid value on an enum field never reaches the server.
--limit=20 — Row count on list commands. The ceiling is written per command in the schema (20 on list commands).
--json — Raw JSON output.
--json — Prints raw JSON. This is the only correct shape for scripts: the default aligned table is for humans, picks its columns from the data and may change layout without notice. Never write a script that parses the table output.
--yes, -y — Passes the confirmation gate (confirm: true over HTTP). Use it only in a script that knows what it is about to do; on an irreversible command this flag is shorthand for "there is no undo".
--idempotency-key= — Makes retrying a writing command safe. A second call with the same key does not re-run the command; it returns the first result. If you build a retry loop, generate the key YOURSELF and send the same one on every attempt.
--refresh — Forces a catalog refresh. lg caches the catalog for an hour; if you know a command was just added on the server, use this instead of waiting.
--help, -h — Prints the command's catalog description, every parameter, its type, its enum values and whether it is required. There is no help file inside the client.
--base-url, --timeout, --verbose, --no-color, --version — Respectively: point one command at another server, a timeout in seconds, request/response detail, disable ANSI colour, and the client version.
Real examples
Every command below exists in the catalog today; no name or parameter here is invented. Outputs are abbreviated. The outputs show the `data.result` level; over HTTP the same object arrives inside the `{success, data:{…}}` envelope (see the HTTP command API).
Loads currently on the road — lg loads list --status=in_transport --limit=20
One load in full, raw JSON — lg loads get --load_code=FSK2158 --json
Open document-consistency flags — lg loads list-problems --only_open=true --limit=10
Resolve the country — lg countries search --query=Germany --json
Filter with the resolved id — lg loads list --receiving_country_id=57 --date_from=2026-09-01 --limit=20
Find an HS code from a product name — lg tariffs search --query="aluminium profile"
Move a load forward (carrier role only) — lg loads set-status --load_code=FSK2158 --status=transit --date=2026-08-21 --yes
Draft — lg export-documents create --doc_type=proforma-invoice --currency_code=EUR --json
Generate it (gated) — lg export-documents generate --document_id=8412 --yes
Without the client: the HTTP command API
lg is a convenience, not the door. Catalog and execution both go through three HTTP endpoints; an ERP or CRM integration connects here directly. Identity is a personal access token carried in Authorization: Bearer. EVERY response comes back in the same envelope: `{ "success": bool, "data": { … }, "code": "…", "message": { … } }`. The whole contract lives inside `data` — `contract_version`, `command`, `status` and, as the command's output, `result`; on a failure `error_code`, `error_key` and `error`. The response examples on this page show the `data` object rather than the envelope, to save space; do not forget to unwrap `data` in your own code. `lg --json` also prints the WHOLE envelope, which is why jq paths start with `.data.result…`. The single exception is `/api/public/status`: that endpoint is envelope-free so watchdogs can alert without parsing it.
Asking for confirmation with 200 + ok:false is forbidden and will never happen. The reason is simple: clients treat 200 as success, so a confirmation request returned as 200 would kill the flow silently and nobody would notice. Confirmation is always 409.
GET /api/common/commands — The whole catalog your role can see: contract_version; per command name, domain, verb, description, JSON Schema, read_only, confirmation_needed, irreversible, since, deprecated_at, replaced_by, aliases; plus the error_keys dictionary, rate_limits and idempotency_ttl_hours. Sends an ETag; If-None-Match answers 304. ?domain= also matches an old (alias) domain. Draw your client from this; do not hard-code a command list.
GET /api/common/commands/{name} — One command in full. name is the frozen command name from the catalog (list_loads), not the CLI identity. A deprecated command answers with an X-Deprecated header.
POST /api/common/commands/{name} — Execution. Body: { args, confirm?, idempotency_key? }. The HTTP status of the response is the result itself — anything other than 200 is information, not noise. Errors carry error_code (transport) and, for business rejections, error_key.
The confirmation gate and irreversible commands
Every command in the catalog carries two flags: confirmation_needed (does it require explicit approval before running) and irreversible (can its effect be undone). They are separate questions, and both are enforced on the server rather than left to the client's good manners.
The gate is about responsibility, not about business rules. Placing a bid is the first step toward a freight contract; issuing an invoice creates an accounting entry; a stock movement changes a ledger. None of these should ever be something you can run twice by accident.
The gate is read from the EFFECTIVE command. When one command wraps another — as the generic executor in the chat does — the gate comes from the flags of the command that will actually run, not from the wrapper. With a fixed flag one of two failures would be inevitable: either the invoice command would run unconfirmed, or every country lookup would raise a confirmation card.
read_only and ungated are not the same thing. There are three distinct states: invoices preview is ungated and writes nothing; loads create is gated and writes; invoices create is ungated but writes — a draft comes into being. One test settles it: when the command returns, has a row in the database changed?
It is technically possible for an integration to pass the gate automatically on every call, but it is not advisable. The right pattern is to automate the gate only for commands you know to be reversible, and to route irreversible ones into a human's queue.
1. Call it — Call the gated command without confirm. Nothing changes.
2. Read the summary — You get a 409; confirmation.summary in the body is a sentence written for a human ("About to issue an invoice: X Ltd, 1,200.00 TRY"), not a dump of raw arguments. Monetary commands also carry a structured preview: line items, totals, warnings.
3. Confirm — Repeat the same call with confirm: true (--yes in the CLI). Irreversible commands are marked so that they run at most once.
Preflight: see what a write command would do, without running it
The most expensive step in wiring an ERP bridge is finding out whether your body is right by ACTUALLY running the command: a draft invoice in the live ledger, a burnt credit, manual cleanup. dry_run ends that — you add one field to the request, the command does not run, and you read what would happen.
The registry holds 207 commands today; 105 of them only read and write nothing. Of the remaining 102 write commands, 85 are gated: called without confirm they already answer 409 and describe what they would do. The gap was the other 17 — invoice drafts, export-document drafts, line items: exactly the chain a bridge wires first. For those, calling was doing.
There was a second gap even on gated commands: schema validation runs AFTER the gate. So a call with a missing argument got the confirmation card first, confirmed it, and only then heard "this field is missing". A preflight answers both in one round trip.
And the most common — and latest-discovered — bridge bug is this: you send a field that is not in the schema (customer_id where recipient_contact_id is expected), the server silently drops it, the command returns 200 and the record lands in the wrong place. The preflight response lists those fields by name under arguments.ignored; no error message ever will.
A preflight writes nothing, and that is a measurement rather than a promise: every SQL statement issued while running EVERY write command (two argument sets, two roles, 178 preflights) was captured. Not one INSERT/UPDATE/DELETE, not one queued job, not one email; row counts across 31 tables and the credit balance were unchanged.
The exit code is the same one the real call would produce — would run 0, confirmation gate 4, idempotency conflict 2, anything else 1 — so "lg … --dry-run || exit" is a meaningful gate in a CI step.
1. Call it with dry_run — Add "dry_run": true to the request body. Validation, the role and scope gate, target resolution and the credit calculation all run; the command does not.
2. Read would_execute — That is the one flag to branch on. When it is false, would_stop.at names where a real call would stop: idempotency, target_precheck, confirmation_gate or arguments.
3. Fix your body — arguments.ignored names the fields that would be silently dropped, missing_required the mandatory ones you left out, and would_charge how many credits would go and whether the balance covers them.
4. Drop dry_run — Repeat until would_execute is true, then remove the flag and make the real call. The idempotency key you sent during the preflight was not consumed, so the real call runs normally.
HTTP is always 200 — "The command would fail" never arrives as a 4xx — the preflight itself succeeded and the prediction is in the body. The exception is a transport refusal, and there it behaves exactly like a real call: 404 unknown command, 403 role/scope, 429 rate limit.
A read-scoped key cannot dry-run a write command — It gets 403. A preflight answers "what happens if I call this WITH THIS KEY, now"; giving a read key a green light and then a 403 on the real call would be precisely the surprise the preflight exists to prevent.
The idempotency key is not consumed — A key sent during a preflight opens no ledger row, and an expired lock is not released. So send the key you intend to use for real: the preflight tells you whether it is still free, would replay an earlier run, or clashes with another command.
It combines with confirm — Send dry_run and confirm together and the question becomes "if I confirm, what happens?". Still nothing runs.
Batch preflight is per batch — commands:batch takes dry_run too, but the flag belongs to the batch, not to an item (sending it on an item is a 422). You get a per-item report plus the batch's total credit cost; an insufficient balance is reported, not refused.
It is not free — Every preflight runs real queries (preview, balance) and counts against the rate limit. Do not preflight the whole catalog on every deploy; preflight the commands you changed.
Idempotency: why retrying does not create duplicates
An ERP does not read your error message, does not see your screen, and will resend the same request after a timeout. This section exists for exactly that behaviour.
A call carrying an idempotency_key runs at most once. The second call does not re-execute the command; it returns the recorded result with status replayed. When the network drops, when a request times out, or when a queue picks up the same job twice, you get the first result instead of a duplicate invoice.
Use a key on every writing command. The catalog's read_only field gives you that distinction for free: every command with read_only: false deserves an idempotency key.
Your retry strategy should use exponential backoff and carry the SAME key on every attempt. Generating a fresh key per attempt is the same as not using idempotency at all. Do not send the same key in parallel: at-most-once is guaranteed for sequential retries, not for two requests racing each other.
Trap 1 — failure is locked in too — If the first execution was rejected by a business rule (failed), a second call with the same key returns the same error and the same error_key, and does not retry. Fixing the argument and trying again requires a NEW key. A key represents an attempt, not an intention.
Trap 2 — a large result replays without a body — The ledger stores a result only if it is under 64 KB. A larger result replays with status replayed but an empty body. Your integration should rely on the "replayed" signal rather than the body, and fetch the data with the matching get command.
Trap 3 — a key belongs to one command — Sending the same key with a different command returns 409 idempotency_conflict with the conflicting_command; nothing runs and nothing is replayed. A client that shares one key across two verbs is broken and learns it from an error, not from a silent replay.
Trap 4 — the lock lasts 24 hours, not forever — After 24 hours the same key is a new execution. Network retries arrive within minutes; an ERP's yearly recurring document numbers must not stay "replayed" forever. The catalog publishes the window as idempotency_ttl_hours.
Trap 5 — a server error does not lock the key — A 5xx server_error is not a business rejection: the key is not bound, so a retry with the same key really runs the command. Only failed locks (trap 1).
Scope and format — your company, any string, prefer a UUID — Uniqueness is enforced on (your company, key): another tenant's INV-1001 never collides with yours and can never replay your result. Use a plain UUID or a compound that is unique by construction such as {ERP-document-no}:{verb}.
Incremental sync and pagination
An ERP/CRM bridge should pull what changed, not everything every hour. Every list command — loads list, demands list-own, contacts list, invoices list, teammates list, load-groups list, reservations list, drivers list, tenders list, products search, warehouses search — accepts two shared parameters and returns one shared field.
Why there is no offset: on a live table an offset drifts. A row inserted between two pages makes you skip or double-count a record, silently. The cursor is forward-only, so a record inserted or updated mid-walk lands at the end of the list: nothing is lost, nothing is duplicated.
A full initial load is the same mechanism: start with updated_since=1970-01-01T00:00:00Z and follow next_cursor until it is null. There is no separate “export” endpoint because none is needed.
Same thing with lg: lg loads list --updated_since=2026-09-01T00:00:00Z --limit=100 --json, then lg loads list --cursor= --json.
For the next sync, set updated_since to the largest updated_at you saw. Timestamps have one-second precision, so a record on the boundary may arrive twice — harmless as long as you upsert by external_ref or code on your side. Without updated_since or cursor a command runs in chat mode: human ordering, at most 20 rows, next_cursor null and truncated saying “there is more”. Do not use that mode for sync.
Machine-readable error keys for this section: INVALID_CURSOR, INVALID_UPDATED_SINCE, DUPLICATE_EXTERNAL_REF, EXTERNAL_REF_TOO_LONG, EXTERNAL_REF_INVALID. All of them answer HTTP 422 with error_code: "failed" and result.error_key in the body. Branch on the key, never on the text.
updated_since — ISO-8601 timestamp (UTC recommended, e.g. 2026-09-01T00:00:00Z). Rows updated at or after this instant. It switches the command into sync mode: fixed order (updated_at, id) ascending, limit capped at 100.
cursor — The next_cursor from the previous response, verbatim. It is opaque and signed: never build or edit one, never reuse another company's or another command's cursor; all of these are rejected with INVALID_CURSOR.
limit — Page size. 1-100 in sync mode (default 10).
next_cursor (response) — Cursor for the next page. null means the list is complete; it is the only correct answer to “is there more”.
total (response) — Rows matching the filters, independent of the page — for progress reporting.
Your own key: external_ref
Do not make our auto-increment ids (invoice_id, contact_id) the business key in your ERP; they are opaque and private to your company. Loads, demands, contacts, invoices, products and warehouses carry your key instead.
Set it when writing — loads create, demands create, contacts create and contacts update, invoices create accept an optional external_ref (max 120 characters) and return it unchanged.
Use it when reading — loads get --external_ref=ORD-2026-0917, invoices get --external_ref=..., contacts get --external_ref=..., demands get --external_ref=... all work; on list and search commands --external_ref= is an exact-match filter.
Unique per company — A second record with the same key is refused with 422 and result.error_key = DUPLICATE_EXTERNAL_REF; another company's key answers “not found” to you. Matching is case-insensitive (ord-1 = ORD-1). An archived record keeps its key; restore it before reusing the key.
Send an idempotency_key too — A retry after a network timeout returns the first result instead of creating a duplicate. The two are not interchangeable: external_ref is your business key, idempotency_key identifies one attempt.
Outgoing webhooks: we POST to you when something happens
Everything up to here was pull: you ask, we answer. An ERP bridge had to ask once a minute to learn that a load changed status, an invoice was issued or a bid arrived. Not any more: register an https address, pick the events you care about, and a signed POST arrives when they happen.
Push was added NEXT TO pull, not instead of it. Incremental sync (updated_since + cursor) is untouched and still the right tool: the first full load, backfilling history and answering "did I miss anything" all live there. Webhooks cut the latency from minutes to seconds. A healthy bridge uses both — the webhook as a trigger, the sync as a safety net.
The payload is a SIGNAL, not a copy of the record. It carries the identifiers (id, your own external_ref, status, time); you fetch the detail with your own key over GET, and that call already passes your own authorisation gates. The exposure surface therefore stays at identifier level. Two fields are deliberately absent: the identity of the company that placed a bid (blind bids appear under an alias in the panel; putting the identity in a webhook would pierce that privacy through a second channel) and reservation contact details (no need to move personal data through an extra channel).
Event names are FROZEN. If the meaning of a name changes, a new name is opened and the old one lives through a deprecation window. Adding a new field to the payload does NOT bump the contract version — a consumer must ignore fields it does not know.
GET /api/common/integration-webhooks — The ledger plus the event catalog, the limits and the header names. Every row carries its muting reason and a 7-day delivery summary.
POST /api/common/integration-webhooks — 201 + { webhook, secret, shown_once: true }. The secret appears only here.
PATCH /api/common/integration-webhooks/{uuid} — Name / address / events / active flag. Does NOT touch the secret.
DELETE /api/common/integration-webhooks/{uuid} — Removes it: the row is soft-deleted (history stays queryable) and pending deliveries are cancelled.
POST /api/common/integration-webhooks/{uuid}/rotate-secret — 201 + a new secret. The old value stops working at that moment.
POST /api/common/integration-webhooks/{uuid}/test — A ping event — real signature, real path, real retry schedule.
GET /api/common/integration-webhooks/{uuid}/deliveries — The most recent deliveries: status, attempt number, HTTP code, duration, error tag.
POST /api/common/integration-webhooks/deliveries/{uuid}/replay — Re-sends a single delivery: event_id is PRESERVED, the delivery id is new, the payload is copied verbatim.
Event catalog
Every one of the 13 events below is backed by an event class that is ACTUALLY dispatched. There are no "would be nice" names in the catalog: an event that is announced but never fired is an event the integrator waits for forever, and that failure is silent. This table is not hand-written either — it is drawn from the live catalog on every page load.
invoice.payment_recorded is deliberately ABSENT: that event class exists in the code but is dispatched from nowhere. The payment event will be added the day the class is genuinely fired — not before. Announcing an event early is worse than never adding it.
load.created — A load became visible to your company (created by you or shared with you). (load)
load.status_changed — A load moved to a new status (pending, in_transport, transit, done). (load)
load.driver_assigned — A driver was assigned to a load. (load)
demand.created — A freight demand was created by your company. (demand)
demand.status_changed — A freight demand changed status. (demand)
demand_bid.created — A bid was placed on a demand you are party to. (demand_bid)
demand_bid.revised — A bid was revised (a new revision replaced the previous one). (demand_bid)
demand_bid.accepted — A bid was accepted. (demand_bid)
invoice.created — An invoice draft was created. (invoice)
invoice.issued — An invoice was issued (it now carries an invoice number). (invoice)
invoice.cancelled — An invoice was cancelled. (invoice)
warehouse_reservation.created — A warehouse dock/area reservation was created. (warehouse)
warehouse_reservation.status_changed — A warehouse reservation was approved, rejected or otherwise moved. (warehouse)
Verifying the signature
Every request carries an X-Logistivo-Signature header: t=,v1=. The signed string is "." and the algorithm is HMAC-SHA256. The v1 prefix keeps the scheme versionable: if a second scheme ever arrives the header carries t=…,v1=…,v2=… and you pick the one you know.
We publish two dependency-free reference receivers, one for Node and one for plain PHP. Both apply the four rules above and test themselves with --selftest: valid signature, tampered body, wrong secret, out-of-window timestamp, malformed header, the re-serialisation trap and both kinds of repetition (same delivery / same fact). The verifier is the exact counterpart of the code we use ourselves — we consume the contract we publish.
Verify the RAW body — The signature is computed over bytes. Decoding and re-serialising the body (JSON.stringify(req.body), json_encode($request->all())) turns the same DATA into different BYTES and the signature stops matching. In Node the trap is number formatting (1.50 on the wire becomes 1.5; large integers lose precision), in PHP it is character escaping (/ becomes \/, İ becomes \u0130). Use express.raw in Express and $request->getContent() in Laravel. This is the single most common mistake when writing a receiver.
Check the timestamp — t is INSIDE the signature. Verifying only the signature lets anyone who captured a valid request replay it hours later, and the receiver would count it as a verified event. Drop any request where |now − t| > tolerance even if the signature is correct. The recommended window is 300 seconds.
Compare in constant time — hash_equals / timingSafeEqual. Comparing with == lets an attacker guess the signature byte by byte.
Deduplicate on event_id, not on id — event_id identifies the FACT: it is the same for two subscribers receiving the same event, and it is PRESERVED on replay. id (= X-Logistivo-Delivery) identifies the DELIVERY and changes on replay. Answer "have I already handled this" with event_id; answering it with id makes a replay run the same work twice.
Delivery, retries and muting
Your receiver will not always be up. The rail assumes that: a transient failure enters the retry schedule, a permanent one mutes the subscription — and the REASON for muting is always recorded.
The muting reason is machine-readable and stable (delivery_failures, endpoint_gone, unsafe_target, key_revoked, by_user) and appears both in the panel and in `lg webhooks list`. Muting without a reason would leave "why is nothing arriving" unanswerable. Re-enabling resets the counter and the stamp.
You write the address; OUR server makes the request. Without a gate, a tenant who entered a cloud metadata address could make us issue requests from inside our own network. So the address is checked both when it is registered and on EVERY delivery:
Timeouts — 8 s total, 4 s to connect — a slow receiver must not occupy a worker. Queue the work and return 2xx immediately.
The one permanent failure: 410 Gone — It means "delete me": no retry, the subscription is muted at once. NEVER return 410 for a signature failure.
Every other failure — ALL of them are retried, 4xx included. Losing a business event costs more than a few extra requests: a receiver returning 403 while a WAF rule is being updated would silently lose an "invoice issued" event on a rail that treats 4xx as permanent.
Retry-After — Honoured on 429 and 503 (capped at 6 hours).
Redirects — NOT followed. Hopping to an internal address via a 302 would pierce the address gate through a second route.
Automatic muting — After 10 consecutive exhausted deliveries. A 410, an unsafe target and the revocation of the linked integration key mute IMMEDIATELY.
localhost, single-label names and internal suffixes such as .internal / .local / .svc are rejected — the intent is rejected before the resolution result is even looked at, so split-horizon DNS cannot pierce the gate;
IP literals and ALL records of the DNS resolution must be public. If a name resolves to both a public and a private address, nobody asks us which one to pick → the name is rejected outright;
at delivery time the address is resolved once and the connection is pinned to the verified address: the DNS rebinding window is closed;
the receiver's response BODY is never stored anywhere (only status code, duration and a short error tag) — so the "read" path, which is what an address-gate hole is actually worth, stays shut.
There is deliberately NO configuration switch to "allow private addresses": such a switch is left on by accident one day.
Returned ONCE, only when the subscription is created and on rotate-secret. It is stored encrypted on the server and can never be read back; there is no "show again".
lg prints it to the screen and leaves it there — it writes it to no configuration or cache file.
If it is lost you rotate it; the old value stops working AT THAT MOMENT. For a seamless cut-over your receiver should try both secrets during the transition.
Changing the address does NOT touch the secret: routine maintenance like "the URL moved" must not break verification on the receiving side.
Health and status: nobody should notice a silent bridge before you do
An integration bridge rarely dies loudly. It usually goes quiet in the small hours: a token expires, the person the key runs on behalf of leaves the team, a scope is narrowed, or the receiving endpoint starts returning 403. The first to notice is almost always the customer. lg status exists to reverse that order.
One command answers two questions at once, because during an outage both are asked at the same moment: is the platform up, and is my bridge healthy. The platform line works without a token — during a failure the token itself is the first suspect, and tying the status question to it would leave you unanswered exactly when you need an answer.
The health summary keeps no new ledger; it folds three existing sources into a single sentence: the daily usage counter (how much you called and how much failed), the request ledger (which request, when, and why it was rejected) and the outgoing webhook delivery ledger (did the event reach the receiver). The error rate is always read from the daily counter; the detail ledger is capped and says so plainly when the cap is reached.
The same summary appears in the panel: under Settings › Integration › Keys every row carries a badge and, beneath it, the reason. Thresholds are not computed client-side; whatever level the server assigned is what both the panel and lg status paint, so the two screens can never say different things about the same key.
Your watchdog needs no token. The endpoint returns status words only; it carries no version, host name, queue name, job count, error text or customer data — and it cannot.
The health summary is produced with a constant number of queries per request; the query count does not grow with the number of keys. For detail, read that key's recent requests with lg logs and the outgoing bridge with lg webhooks list.
HEALTHY — No measured fault and we hold proof of success: a successful call in the ledger, or traffic that stayed error-free in the counter.
ATTENTION — Something started to break: the error rate or the consecutive-failure streak crossed a threshold, requests are being rejected at the gate, the key is close to expiry, or a bridge that spoke regularly went quiet.
FAILING — The bridge is not working right now: the token was revoked unintentionally, a closed key is still being called, there are five failures in a row, the error rate is above fifty percent, or a webhook subscription was muted.
CLOSED — The key was closed deliberately and nobody is calling it. That is a decision, not a fault — and it is not painted red.
NO DATA — There is no measurement: the key has never been called, or the measurement itself failed. Neither of those is green.
No false green. The healthy badge requires measured proof of success; a key that has never been called never turns green, because not a single measurement shows that it works.
No false red. Keys you closed on purpose show as Closed. Painting everything red makes red meaningless; a closed key turns red only if it is still being called after closure — and then a bridge really is broken.
Could not measure is not the same as no data. If a query fails server-side the row does not quietly show zero; it says the measurement could not be taken.
Silence is always reported but never blindly turned into a warning. A month-end bridge going quiet for thirty hours is normal; a bridge that spoke on at least five of the last seven days going quiet is a fault. The rhythm is measured, not assumed.
The status query itself does not pollute the health numbers. Calls made by lg status appear in the request ledger but are excluded from the health maths — otherwise your own successful status query would reset the failure streak and hide the very fault the screen exists to show.
Every line is measured: a real query is run against the database, the command catalogue is really built, the live record of the queue worker is really read. Nothing is assumed.
The response itself is a measurement too: if you can read the body, the web tier is up.
The top level derives only from required components. When the queue stops the answer is degraded, not down — a synchronous API call does not need the queue, and declaring the platform down would be a false alarm.
When the queue state cannot be measured the top level is left intact: not knowing is not the same as measuring that something is broken.
If the platform is down the endpoint returns HTTP 503. The body is envelope-free so monitoring tools can alert without parsing it, and the response is never stored in any cache.
Errors and exit codes
What a machine branches on is a stable string. The text in the error field targets a human, is localised, and changes without notice — an integration that branches on message text is not supported, and its breaking is not a bug.
Two taxonomies, two fields. error_code is the transport layer (the table above) and is what you branch on first. error_key is the stable business-rule key inside failed — INSUFFICIENT_CREDITS, NOT_FOUND, DUPLICATE_CONTACT, INSUFFICIENT_STOCK — published as a dictionary in the catalog (error_keys) and below. A command that does not yet emit a key returns error_key: null; treat a key missing from the dictionary as a generic failure.
ok — HTTP 200 · error_code — · CLI 0 · —
replayed — HTTP 200 · error_code — · CLI 0 · Idempotent replay; the work did NOT run again.
failed — HTTP 422 · error_code failed · CLI 1 · No — a business rule rejected it (see error_key). Pointless until the argument changes.
idempotency_conflict — HTTP 409 · error_code idempotency_conflict · CLI 2 · No — the key was first used for a different command (conflicting_command). Mint a new key.
forbidden — HTTP 403 · error_code forbidden · CLI 3 · No — role gate, or a read-scoped key on a writing command.
not_found — HTTP 404 · error_code not_found · CLI 2 · No — wrong or removed command name.
(schema) — HTTP 422 · error_code invalid_request · CLI 2 · No — the request body or an argument failed schema validation (errors{}); not a business rule.
server_error — HTTP 500 · error_code server_error · CLI 6 · Yes — exponential backoff with the SAME idempotency key; the key was not locked.
401 unauthenticated — The token expired or was revoked. Retrying will not help; the token must be replaced.
429 rate_limited — Rate limited for this token. The response always carries Retry-After and a body with retry_after, limit and bucket; honour it instead of inventing a fixed delay. lg waits and retries up to three times, then exits 5.
5xx server_error / network error — Retry with exponential backoff and the SAME idempotency key. Changing the key re-runs work the server may already have completed. lg retries three times (1 s, 2 s, 4 s), then exits 6 with error_code network_error or server_error.
Stability promise
The most expensive question in an integration contract is “will the code I write today still run tomorrow?” One number answers it: contract_version in the catalog, and the X-Contract-Version header on every response. It answers exactly one question — does code written earlier still run unchanged? If the number did not increase, yes. If it did, no: something you relied on was removed or narrowed.
Because an announcement removes nothing: your existing code runs exactly as before, the day after it. Bumping on announcement would emit two signals for one change and train you to ignore the first — weakening the signal that actually matters. The announcement already rides on the call you are making: deprecated_at and replaced_by on the catalog row, an X-Deprecated header plus a deprecated block on that command's response, and a one-line stderr warning if you use lg. You do not have to do anything to learn that something is going away; it is already in your log.
At least two calendar quarters pass between announcement and removal. Throughout the window the old command still answers, and warns on every call. When a CLI name is retired, its successor takes that name over as an alias: during the window the old name reaches the old command, and the day it is removed the same call lands on the new one — without you shipping a client release.
preview_invoice → get_invoice (announced 3 September 2026). get_invoice returns the same preview block and adds the line items, the tax breakdown, recorded payments, the remaining balance and addressing by your own ERP key (external_ref). preview_invoice keeps working, and so does lg invoices preview — both of them warn.
no change — A new command · a new OPTIONAL parameter · a new response field · a new error key · a new endpoint — Do nothing. You must ignore fields you do not know; a parser that fails on an unknown field is non-conforming.
no change — A deprecation ANNOUNCEMENT: notice that a command is going away — Your code keeps running. Put a task in your calendar — the window is at least 6 months.
increases — Removing a command · retiring an old CLI name · dropping a response field · making an optional parameter required · narrowing an enum — It means you missed the window; the notice reached you in three separate places.
A command's API name (such as list_loads) is frozen forever; that is the name you use in HTTP calls.
error_code (transport) and error_key (business rule) are stable strings. New ones may appear — treat a key you cannot find in the dictionary as a generic failure, not as a breakage.
The mapping between HTTP status and the status field in the body is fixed.
The version number never decreases, and is readable on every response — including bodyless 304 and 429.
Error and description TEXT. It is localised and changes without notice; branching on message text is not supported.
The absence of fields you do not know. Adding a field does not bump the version.
Auto-increment ids as business keys. Carry your own key with external_ref.
Dynamic error keys derived from class names; they are not part of the contract.
Command identity table
The table below is not hand-written: it is generated from the live command registry every time this page is served. A command added on the server appears here on its own. The "Gate" column tells you whether a command asks for confirmation and whether its effect can be undone; the "Roles" column tells you which account types will find it in their catalog.
The API name (list_loads) is frozen and will not change; it is the name you use in HTTP calls. The CLI identity (loads list) is governed by the versioning policy: once announced it changes only with a version bump and a migration window, during which the catalog serves the old pair as an alias. The descriptions here come from each command's own record and are shortened — for the full text, parameter types and enum values, read the catalog endpoint or run lg help .
lg accounting-settings get — Read the company's accounting and invoicing settings and say whether they are set up (muhasebe ayarları, fatura ayarları, e-fatura / e-invoice profile, KDV / VAT tax regime, fatura numaralandırma serisi / invoice… (read · — · all · API: get_accounting_settings)
lg accounting-settings update — Update the company's accounting/invoicing settings (muhasebe ve fatura ayarlarını değiştir): legal name, default currency, provider (local/parasut), country, tax regime (KDV/VAT/GST/none), default invoice line… (write · confirm · all · API: update_accounting_settings)
lg attachments get — Read a file the user attached to THIS conversation (listed in the user message as [Attachments] with their attachment_uuid). (read · — · all · API: read_attachment)
lg bank-accounts create — Add one of YOUR OWN company bank accounts to the account book (the accounts you get paid into). (write · confirm · all · API: create_company_bank_account)
lg bank-accounts list — List bank accounts. (read · — · all · API: list_bank_accounts)
lg bank-accounts set-status — Confirm or reject a bank account that was read from a document by AI. (write · confirm · all · API: decide_bank_account_verification)
lg bids accept — Accept a carrier's bid on one of this company's demands. (write · confirm + irreversible · shipper · API: accept_demand_bid)
lg bids create — Submit a bid (teklif ver) on an open demand. amount_per_vehicle is the freight PER VEHICLE, not the total. (write · confirm + irreversible · carrier · API: create_demand_bid)
lg bids list — List the bids (teklif) THIS carrier company has submitted, newest first, with their outcome. (read · — · carrier · API: list_my_bids)
lg bids revise — Revise this carrier's own PENDING bid on a demand: the old bid is marked revised and a NEW bid replaces it (it gets a new bid code). (write · confirm + irreversible · carrier · API: revise_demand_bid)
lg carbon-reports calculate — Calculate the CO2 emission of an EXISTING load from its own data: the weight on the load, the route between its first pickup and last delivery address (including the Ro-Ro leg when the load has one), the distance, the… (read · — · all · API: calculate_load_carbon_emission)
lg carbon-reports get — Read one CO2 emission report in full: total emission in kg, transport mode, weight, distance, emission factor, calculation method and source, the departure/arrival addresses, the leg breakdown for multi-modal routes… (read · — · all · API: get_carbon_emission_report)
lg carbon-reports list — List the CO2 emission reports this company has generated (newest first): report number, file name, who created it and when. (read · — · all · API: list_carbon_emission_reports)
lg chart-of-accounts list — List the chart of accounts available to this company (Turkish uniform chart: 100 Kasa, 102 Bankalar, 120 Alıcılar, 320 Satıcılar, 391 Hesaplanan KDV, 600/601 Satışlar…), optionally filtered by code/name text or type. (read · — · all · API: list_chart_of_accounts)
lg checks bounce — Record that a cheque BOUNCED (came back unpaid). (write · confirm + irreversible · all · API: mark_check_bounced)
lg checks collect — Record that a cheque was COLLECTED (money reached the bank for an incoming cheque, or left the bank for an outgoing one) on its due date. (write · confirm + irreversible · all · API: mark_check_collected)
lg checks get — Read one cheque in full: its identity (number, serial, bank and branch, drawer with tax number, IBAN), the face amount and currency, issue and due dates, the counterparty, the status with every timestamp (executed,… (read · — · all · API: get_settlement_check)
lg checks list — List the cheque portfolio of this company: cheque number, direction (in = received from a customer, out = given to a supplier), drawer and bank, face amount with its currency, issue and DUE date, the counterparty… (read · — · all · API: list_settlement_checks)
lg company-documents list — List this company's own filed documents (tax certificate, signature circulars, trade registry gazette, contracts, TIO/L2 permits, files uploaded to chats…) with their type, number, validity date and verification status. (read · — · all · API: list_company_documents)
lg contacts add-bank-account — Add a bank account (IBAN) to a business contact so it can be used on payment instructions and invoices. (write · confirm · all · API: add_business_contact_bank_account)
lg contacts create — Create a new business contact (cari) in the company directory. (write · confirm · all · API: create_business_contact)
lg contacts get — Read one business contact in full (identity, tax data, address, e-mail/phone, bank accounts, platform link) together with its account summary: ledger balance (positive = they owe us), open receivables and open payables. (read · — · all · API: get_business_contact)
lg contacts list — List the company's business contacts (customers, carriers, suppliers, consignees) with optional text search over name, legal name and tax number. (read · — · all · API: list_business_contacts)
lg contacts search — Resolve a business contact (customer/partner/recipient) by name, legal name or tax number to its contact_id. (read · — · all · API: lookup_business_contact)
lg contacts update — Update fields of an existing business contact. (write · confirm · all · API: update_business_contact)
lg conversations add-message — Post a message into a load/bid conversation AS THIS USER (not as an assistant). (write · confirm + irreversible · all · API: send_conversation_message)
lg conversations list — List the chat conversations this user is a member of (load chats, bid chats and private carrier channels), newest activity first. (read · — · all · API: list_conversations)
lg conversations list-messages — Read the most recent messages of one conversation — give it a load code (e.g. (read · — · all · API: list_conversation_messages)
lg conversations list-participants — Who is in this conversation — name and company of every participant. (read · — · all · API: list_conversation_participants)
lg conversations mark-read — Mark one conversation as read for this user (clears its unread badge). (write · — · all · API: mark_conversation_read)
lg conversations remove-message — Delete one of THIS user's own messages from a conversation (message_id from list_conversation_messages with `mine` true). (write · confirm + irreversible · all · API: delete_conversation_message)
lg conversations update-message — Edit one of THIS user's own messages in a conversation (message_id comes from list_conversation_messages, where `mine` is true). (write · confirm · all · API: update_conversation_message)
lg countries search — Resolve a country by name (Turkish, English or the local/native name) or ISO 3166-1 alpha-2 code to its country_id, for the sending/receiving country on a load. (read · — · all · API: lookup_country)
lg demand-targets search — Resolve a pickup/delivery location for a demand to a target token ("5-" = district/postal area, "10-" = seaport address). (read · — · shipper · API: lookup_demand_target)
lg demands create — Open a freight demand (talep) so carriers can bid. (write · confirm + irreversible · shipper · API: create_demand)
lg demands get — Detail of ONE demand owned by this customer company, INCLUDING the bids it received (bid_code, carrier name — masked as an alias when the carrier chose to stay hidden — price per vehicle, total, currency, vehicle count,… (read · — · shipper · API: get_demand)
lg demands list — List freight demands (talep) this carrier is allowed to bid on, newest first. (read · — · carrier · API: list_open_demands)
lg demands list-own — List the freight demands (talep) OWNED by this customer company, newest first, with route, dates, vehicles, status and bid count. (read · — · shipper · API: list_demands)
lg demands options — Reference lists needed to build a freight demand (talep) with create_demand: vehicle types (id, name, transport mode), payment maturity options (id + days; company_default_maturity_option_id is what the company normally… (read · — · shipper · API: get_demand_form_options)
lg demands set-status — Close (make passive) one of this company's ACTIVE demands so carriers stop bidding. (write · confirm + irreversible · shipper · API: close_demand)
lg document-checks run — Cross-check documents the user attached to THIS conversation against each other and report the discrepancies: mismatched party names, quantities, weights, amounts, dates, container or reference numbers between an… (write · — · all · API: check_document_compliance)
lg documents download — Get a short-lived (15 minute) download/preview link for one document by its document_id (from list_load_documents or list_company_documents). (read · — · all · API: get_document_link)
lg driver-accounts get — Answer "how much do we owe this driver / how much does the driver owe us". (read · — · shipper, carrier · API: get_driver_account_summary)
lg driver-advances create — Record a money movement between the company and a driver: an advance handed to the driver, money the driver gave back, a closing payout, or a carry-forward opening balance. (write · confirm · shipper, carrier · API: create_driver_advance)
lg driver-advances list — List the money movements between the company and its drivers: cash advances handed out, money paid back by the driver, closing payouts and carry-forward opening balances. (read · — · shipper, carrier · API: list_driver_advances)
lg driver-expenses create — File one or MANY expense receipts the user attached to this conversation (fuel, toll, ferry, parking, repair, meal…) into the driver expense ledger as pending expenses of ONE of this company's drivers. (write · confirm + irreversible · shipper, carrier · API: process_receipt)
lg driver-expenses get — Read one driver expense (receipt) by its id: merchant, amount, currency, converted amount, category, payment channel, whether it is charged to the driver, and the approval status. (read · — · shipper, carrier · API: get_driver_expense)
lg driver-expenses list — List driver expense receipts (fuel, toll, ferry, parking, food, repair…) with a converted total. (read · — · shipper, carrier · API: list_driver_expenses)
lg driver-expenses review — Approve or reject ONE driver expense (receipt) from the chat, or send an already-decided one back to pending. status=approved means the company accepts the expense (an approved reimbursable receipt can then be counted… (write · confirm · shipper, carrier · API: review_driver_expense)
lg driver-statements cancel — Cancel a statement that should not exist (opened by mistake, duplicated). (write · confirm + irreversible · shipper, carrier · API: cancel_driver_account)
lg driver-statements create — Open a NEW draft settlement statement for a driver. (write · confirm · shipper, carrier · API: create_driver_account)
lg driver-statements finalize — FINALISE a draft settlement statement — it freezes the document, makes it visible to the driver and (for settle_cash) writes the counter-entry into the advance ledger. (write · confirm + irreversible · shipper, carrier · API: finalize_driver_account)
lg driver-statements get — Read one driver settlement statement in full: its status and revision, the driver, the currency, every total (expenses, advances, repayments, receiptless declarations), the NET balance with the sentence that says which… (read · — · shipper, carrier · API: get_driver_account)
lg driver-statements list — List the driver settlement statements (mutabakat belgesi) of this company: code, which driver, status (draft / finalized / cancelled), currency, the net balance and what it means, how many expense and advance lines it… (read · — · shipper, carrier · API: list_driver_accounts)
lg driver-statements reopen — Reopen a FINALISED statement so it can be corrected (a late receipt arrived, a line was wrong). (write · confirm · shipper, carrier · API: reopen_driver_account)
lg driver-statements update-lines — Attach receipts/advances to a DRAFT statement, or detach ones that should not be on it. (write · confirm · shipper, carrier · API: update_driver_account_lines)
lg drivers add-document — Record a driver paper — visa, passport, residence permit, driving licence, SRC certificate, psychotechnic certificate, travel health insurance — for ONE of this carrier's drivers, so its expiry is tracked. (write · confirm · carrier · API: add_driver_document)
lg drivers archive — Archive (deactivate) a driver: their account is disabled, open mobile sessions are revoked and the login link stops working. (write · confirm + irreversible · carrier · API: archive_driver)
lg drivers create — Create a new driver account for this carrier company. (write · confirm · carrier · API: create_driver)
lg drivers delete-document — Remove a driver paper from the driver's file. (write · confirm + irreversible · carrier · API: delete_driver_document)
lg drivers get — Full detail of ONE driver of this carrier: identity, archive state, mobile app state, last GPS position, assigned load codes, ALL driver documents (passport, visa, licence, SRC… with expiry and days_to_expiry) and the… (read · — · carrier · API: get_driver)
lg drivers list — List this carrier company's drivers with their status, current country, expiring/expired document counts, assigned load codes and vehicle plates. (read · — · carrier · API: list_drivers)
lg drivers list-documents — List ONE driver's documents (passport, visa, residence permit, licence, SRC, ADR, health insurance…) with document number, issuing country, valid_until, days_to_expiry, is_expired and AI extraction status. (read · — · carrier · API: list_driver_documents)
lg drivers login-link — Produce the driver's 48-hour mobile app login link (sensitive credential). (write · confirm · carrier · API: get_driver_login_link)
lg drivers search — Resolve one of this company's drivers by name to its driver_id. (read · — · carrier · API: lookup_driver)
lg drivers update — Correct a driver's first/last name. (write · confirm · carrier · API: update_driver)
lg drivers update-document — Correct a driver paper already on file — most often its expiry date (valid_until) after a renewal, but also the document number, the issuing country, the type or the note. (write · confirm · carrier · API: update_driver_document)
lg equipment-damages list — List the equipment/trailer damages recorded for this company: which vehicle, where on it (area and angle), damage type and severity, whether it is still open or repaired, the estimated and actual repair cost, who is… (read · — · all · API: list_equipment_damages)
lg equipment-inspections list — List the equipment hand-over inspections (teslim-tesellüm controls) of this company: which vehicle, the event type (hand-over, take-over, interim), when and where it happened, who performed it (driver or office user, or… (read · — · all · API: list_equipment_inspections)
lg export-documents add-item — Append a line item to an export document. (write · — · all · API: add_export_document_item)
lg export-documents create — Create a new export document draft. (write · — · all · API: create_export_document_draft)
lg export-documents extract — Read a pasted order e-mail, offer or confirmation and fill an export document from it. (write · — · all · API: extract_export_document_from_text)
lg export-documents generate — Produce the final PDF of an export document AND return a short-lived signed download link for it, so the user gets the file straight from the chat. (write · confirm + irreversible · all · API: generate_export_document)
lg export-documents get — Read one export document in full: every filled field as dot-paths, the line items with their 1-based positions, the server-computed totals and which required fields are still empty. (read · — · all · API: get_export_document)
lg export-documents list — List the export documents of the current company (proforma invoice, commercial invoice, packing list, shipping instruction, delivery note, certificate/movement applications, exporter declaration, insurance request). (read · — · all · API: list_export_documents)
lg export-documents remove-item — Delete one line item by its 1-based position. (write · — · all · API: remove_export_document_item)
lg export-documents set — Set one or more fields on an export document draft. (write · — · all · API: set_export_document_fields)
lg export-documents update-item — Change columns of one existing line item, addressed by its 1-based position from get_export_document. (write · — · all · API: update_export_document_item)
lg finance get — One call that returns the measured financial BASE for a period — the numbers a budget, a forecast, a business plan, a cash-flow projection or a board summary is built on, instead of chaining six separate reads. (read · — · all · API: get_financial_snapshot)
lg fleet-documents list — List fleet documents that expire soon (or already expired): vehicle papers (insurance, inspection, permits) and — for carriers — driver papers (passport, visa, licence, SRC). (read · — · shipper, carrier · API: list_expiring_documents)
lg fuel-cards create — Register a fuel card (DKV, UTA and the like) in the fleet card book, optionally tied to a driver or a vehicle. (write · confirm · shipper, carrier · API: create_fuel_card)
lg fuel-cards list — List the company's fuel and toll cards (DKV, UTA, Eurowag, E100, Shell, OMV, Aral, Petrol Ofisi, Opet…) with the last four digits, the driver or vehicle they are tied to, and whether they are active. (read · — · shipper, carrier · API: list_fuel_cards)
lg fuel-cards update — Update a fuel card: move it to another driver or vehicle, change its label or note, or DEACTIVATE it (is_active false) when the card is cancelled. (write · confirm · shipper, carrier · API: update_fuel_card)
lg import-duties get — Read the Turkish import duty table for one GTİP code: customs duty (İthalat Rejimi Kararı) and additional customs duty / İGV, with every country-group column, plus the statutory duty from the nomenclature. (read · — · all · API: get_import_duty_rates)
lg invitations create — Invite a colleague to this company by e-mail. (write · confirm + irreversible · all · API: invite_teammate)
lg invitations list — List teammate invitations of this company that are not completed yet (pending / e-mail verified / account created), with invitation_code, e-mail, role and expiry. (read · — · all · API: list_pending_invitations)
lg invitations resend — Re-send a pending teammate invitation e-mail and extend its expiry by 14 days. (write · confirm + irreversible · all · API: resend_invitation)
lg invitations revoke — Cancel a pending teammate invitation so its link stops working (it can later be re-sent). (write · confirm · all · API: revoke_invitation)
lg invoice-settings get — Read the invoice document settings (fatura ayarları, fatura şablonu ayarları): which invoice templates exist, which template configuration is the company default, which fields of it are filled (logo, seller address,… (read · — · all · API: get_invoice_settings)
lg invoices add-line — Add a line item to a DRAFT invoice and recompute totals (tax auto-resolved if tax_rate_id omitted). (write · — · all · API: add_invoice_line)
lg invoices add-payment — Record money received (sales invoice) or paid (purchase invoice) against ONE issued invoice: writes the payment, posts the balanced journal entry and reduces the remaining balance. (write · confirm + irreversible · all · API: record_invoice_payment)
lg invoices cancel — Cancel an ISSUED invoice this company issued: posts the reversing journal entry and marks the invoice cancelled. (write · confirm + irreversible · all · API: cancel_invoice)
lg invoices create — Create a DRAFT invoice (reversible) issued by the current company to a recipient business contact. (write · — · all · API: create_invoice_draft)
lg invoices delete-draft — Delete a DRAFT invoice of this company together with its lines and taxes. (write · confirm + irreversible · all · API: delete_invoice_draft)
lg invoices download — Get a short-lived download link for the PDF of an invoice visible to this company (and the e-invoice XML when one exists). (read · — · all · API: download_invoice_pdf)
lg invoices get — Read one invoice in full: parties, load link, dates, every line with tax, tax breakdown, totals, remaining balance and the payments recorded against it. (read · — · all · API: get_invoice)
lg invoices issue — Issue (finalize) a DRAFT invoice: assigns a number and posts accounting entries. (write · confirm + irreversible · all · API: issue_invoice)
lg invoices list — List invoices visible to this company — the ones it issued and the ones issued to it (as a linked party). (read · — · all · API: list_invoices)
lg invoices preview — Show the user a full preview of a DRAFT invoice (recipient, line items, tax breakdown, totals) WITHOUT issuing it. (read · — · all · API: preview_invoice · deprecated: 2026-09-03 · use get_invoice)
lg invoices reverse-payment — Undo ONE payment/collection recorded on an invoice: posts the reversing journal entry, gives the amount back to the invoice's remaining balance and removes the payment row. (write · confirm + irreversible · all · API: reverse_invoice_payment)
lg journal-entries create — Post a manual double-entry journal entry (yevmiye fişi). (write · confirm + irreversible · all · API: create_journal_entry)
lg journal-entries get — Read one journal entry with its lines (account code/name, debit, credit, counterparty) and totals. (read · — · all · API: get_journal_entry)
lg journal-entries list — List journal entries (yevmiye fişleri) of the company, newest first, with optional date window. (read · — · all · API: list_journal_entries)
lg load-documents create — File one or MANY documents the user attached to this conversation onto a load: each file is stored as a company document and posted into the load's chat, where every participant (customer, carrier, customs broker) sees… (write · confirm + irreversible · all · API: attach_chat_file_to_load)
lg load-documents list — List the documents (CMR, invoice, packing list, customs papers, photos…) filed on a load — i.e. the files shared in that load's chat. (read · — · all · API: list_load_documents)
lg load-groups add-loads — Merge EXISTING loads into a groupage trip you own as consignments (they keep their own code, chat, documents and participants; only the trip membership changes — nothing is duplicated and NO credit is used). (write · confirm · shipper, carrier · API: add_loads_to_group)
lg load-groups create — Create a groupage trip (parsiyel/sefer): ONE truck carrying several ISOLATED consignments; each consignment is its own load with its own chat/documents/status and receivers never see each other. (write · confirm + irreversible · shipper, carrier · API: create_load_group)
lg load-groups get — Detail of one groupage trip: consignments visible to you (load code, status, receiver, route, driver), planned date, optimised master route summary and stop count. (read · — · shipper, carrier · API: get_load_group)
lg load-groups list — List groupage trips (parsiyel / sefer: one truck, several isolated consignment loads) this company owns or — for carriers — carries, newest first, with visible consignment count and route optimisation state. (read · — · shipper, carrier · API: list_load_groups)
lg load-groups optimize — Re-optimise the master route of a groupage trip you own and RETURN the new itinerary: the ordered stops (pickup → export customs → import customs → dropoff), each stop's address and consignment codes, the total distance… (write · — · shipper, carrier · API: optimize_trip_route)
lg load-groups remove-load — Take one consignment out of a groupage trip you own. (write · confirm · shipper, carrier · API: remove_load_from_group)
lg load-types search — Resolve a load/transport type (e.g. (read · — · all · API: lookup_load_type)
lg load-vehicles delete — Detach the fleet equipment from a load: both slots by default, or only the tractor unit / only the trailer. (write · confirm · all · API: clear_load_equipment)
lg load-vehicles get — Show which tractor unit (or truck/van) and which trailer are attached to a load, whether they belong to your own fleet, and whether the load can still be changed. (read · — · all · API: get_load_equipment)
lg load-vehicles set — Attach one of your own fleet vehicles to a load: the tractor unit / truck slot (plate) and/or the trailer slot (trailer_plate). (write · confirm · all · API: assign_vehicle_to_load)
lg loads assign-driver — Assign one of this company's drivers to a load so the driver sees it in the mobile app and starts reporting position. (write · confirm · carrier · API: assign_driver_to_load)
lg loads audit-documents — Cross-check the documents already attached to loads and report, PER LOAD, whether their package counts and gross weights agree. (read · — · all · API: audit_load_documents)
lg loads close-problem — Close the open document-consistency flag of ONE load, from the chat. action=resolved means the contradiction was actually fixed (a corrected document was uploaded); action=dismissed means it was a false alarm and should… (write · confirm · all · API: resolve_load_problem_flag)
lg loads create — Open a freight load (yük). (write · confirm + irreversible · all · API: create_load)
lg loads get — Read one load in full by its code: route, dates, weight, current status, parties (sender/receiver/carrier), latest reported position and any open AI document-consistency flag. (read · — · all · API: get_load)
lg loads list — List the freight loads (yük) this company can see, newest first. (read · — · all · API: list_loads)
lg loads list-problems — List loads whose uploaded documents the AI consistency check found to CONTRADICT each other (e.g. invoice weight vs CMR weight). (read · — · all · API: list_load_problem_flags)
lg loads set-status — Move a load to a new transport status and append it to the load's status history (this is what the customer sees on the tracking screen). (write · confirm + irreversible · carrier · API: update_load_status)
lg notifications list — Read the signed-in user's own notification inbox (the bell icon): what happened, which kind of event it was, when, whether it is still unread, and the panel address the notification points at. (read · — · all · API: list_notifications)
lg notifications mark-read — Mark ALL of the signed-in user's unread notifications as read (empty the bell). (write · — · all · API: mark_notifications_read)
lg packages get — Read this company's package and CREDIT balance: how many credits are left, how many the package grants per year, how many were used, the renewal date, and the assistant token usage of the current period (1,000,000… (read · — · all · API: get_credit_balance)
lg pallets add-measurement — Record a NEW measurement for an existing pallet (dimensions in centimetres and/or gross weight in kilograms). (write · confirm · all · API: add_pallet_measurement)
lg pallets create — Register a NEW pallet in a warehouse, optionally with its first measurement. warehouse_id is required — resolve the warehouse name with lookup_warehouse first. (write · confirm · all · API: create_pallet)
lg pallets extract — Estimate a pallet's content label, dimensions (cm) and gross weight (kg) from photo(s) the user attached to THIS conversation. (write · — · all · API: extract_pallet_from_photos)
lg pallets get — Read one pallet in full: its identity (code, label, SKU, barcode), status, warehouse and area, the FULL measurement history (each row with its source — manual, lidar, arcore or ai_photo — and confidence), how many proof… (read · — · all · API: get_pallet)
lg pallets list — List the pallets of this company: code, label, SKU/barcode, status, which warehouse and area it sits in, its latest measured dimensions (cm) and gross weight, and how many proof photos it has. (read · — · all · API: list_pallets)
lg pallets update — Update an existing pallet: its status (in_stock, in_transit, reserved, shipped, disposed), its label/SKU/barcode/note, or MOVE it to another warehouse or area. (write · confirm · all · API: update_pallet)
lg payables list — Payables ("whom do we owe how much"): per-supplier totals of recorded purchase invoices — invoiced, paid, remaining, earliest due date — with a per-currency breakdown and an overall summary. (read · — · all · API: list_payables)
lg payment-instructions get — Read one bank payment instruction: its code, template, effective date, status with timestamps and cancel reason, its note, and — when the PDF has already been rendered — a short-lived signed link to it so the user gets… (read · — · all · API: get_settlement_instruction)
lg payment-instructions list — List the bank payment instructions of this company: instruction code, which template (bank format) it uses, its effective date, status (pending, executed, cancelled) and when it was executed or cancelled. (read · — · all · API: list_settlement_instructions)
lg plans archive — Close a plan because the user says the work is finished or abandoned. (write · — · all · API: archive_plan)
lg plans create — Write down a multi-step job so it survives between conversations ("link these 5 loads to a trip, then invoice them"). (write · confirm · all · API: create_plan)
lg plans get — Read one plan: the steps that were agreed AND the commands that actually ran while it was active. (read · — · all · API: get_plan)
lg plans list — List this company's plans, newest first. (read · — · all · API: list_plans)
lg preferences create — Remember a lasting preference of THIS user, so it applies to every future conversation too ("always show amounts in EUR", "write driver names surname first", "I want the weekly summary on Mondays"). (write · — · all · API: remember_preference)
lg preferences delete — Forget one stored preference of this user. (write · — · all · API: forget_preference)
lg preferences list — List what you have been asked to remember about THIS user, with the labels. (read · — · all · API: list_preferences)
lg products create — Define a new product in this company's catalogue so stock can be recorded against it: name plus optionally its unit of measure, SKU, barcode, category, brand, minimum stock level, reorder quantity and lot tracking. (write · confirm · shipper · API: create_product)
lg products list — List the products of this company: code, name, SKU/barcode, unit, category and brand, whether it is active, and its low-stock threshold. (read · — · shipper · API: list_products)
lg products list-low-stock — List the products that have fallen to or below their low-stock threshold, with the quantity on hand and the threshold itself. (read · — · shipper · API: list_low_stock_products)
lg products search — Resolve a product to its product_id + on-hand quantity. (read · — · shipper · API: lookup_product)
lg products update — Correct an existing product: its name, unit, SKU, barcode, category, brand, minimum stock level, reorder quantity, lot tracking, note, or whether it is active. (write · confirm · shipper · API: update_product)
lg receivables get — Account statement of one contact: every sales invoice issued to them and every collection, in date order with a running balance, plus totals (invoiced, paid, open balance, open invoice count). (read · — · all · API: get_receivable_statement)
lg receivables list — Receivables ("who owes us how much"): per-contact totals of issued sales invoices — invoiced, paid, remaining — with a per-currency breakdown and an overall summary. (read · — · all · API: list_receivables)
lg reports get — Financial reports from posted journal entries: trial_balance (per-account debit/credit/balance as of a date), balance_sheet (as of a date) or income_statement (revenues/expenses/net income for a date range; defaults to… (read · — · all · API: get_accounting_report)
lg reservations approve — As the warehouse owner, approve a pending reservation received at your warehouse (the booker is notified). (write · confirm · shipper · API: approve_reservation)
lg reservations cancel — Cancel (withdraw) a warehouse reservation you booked, or — as the warehouse owner (customer) — cancel a reservation received at your warehouse. (write · confirm + irreversible · all · API: cancel_reservation)
lg reservations create — Book a warehouse dock time slot. (write · confirm · all · API: create_reservation)
lg reservations get — Detail of one warehouse reservation you booked or (customer) one received at your warehouse: warehouse, area, time window, status, contact, notes, rejection reason. (read · — · all · API: get_reservation)
lg reservations list — List warehouse dock reservations. side=booked_by_me (default): reservations this user/company booked at other companies' warehouses. side=received (customer role only): reservations other companies made at ONE of your… (read · — · all · API: list_reservations)
lg reservations list-slots — For a warehouse reservation link (the token at the end of a https://…/public/reservation/ link a warehouse owner shared), show the warehouse (name, owner company, working hours, rules) and the available dock time… (read · — · all · API: list_reservation_slots)
lg reservations reject — As the warehouse owner, reject a pending reservation received at your warehouse, with an optional reason shown to the booker. (write · confirm + irreversible · shipper · API: reject_reservation)
lg roro-routes list — For a load you are carrying, list the Ro-Ro (ferry) lines closest to its PICKUP address, nearest first: the line name, its departure and arrival ports and how many kilometres the pickup point is from the departure port. (read · — · carrier · API: list_nearby_roro_routes)
lg roro-routes select — Select the Ro-Ro (ferry) line a load you are carrying will use. (write · confirm · carrier · API: select_roro_route)
lg roro-routes set-required — Mark whether a load you are carrying will travel by Ro-Ro (ferry) or not. (write · confirm · carrier · API: set_load_roro_required)
lg settlements create — Close several open invoices of ONE contact in ONE currency in a single bulk collection (direction in) or bulk payment (direction out). instrument transfer = money already moved by bank transfer (payments + journal… (write · confirm + irreversible · all · API: create_settlement_batch)
lg settlements list — List bulk collection/payment batches (code, direction, instrument, contact, currency, total, value date, status), newest first. (read · — · all · API: list_settlement_batches)
lg settlements list-invoices — Open LOCAL invoices of one contact in one currency that can be allocated in a bulk collection (direction in = sales invoices) or bulk payment (direction out = purchase invoices), each with payable_remaining (remaining… (read · — · all · API: list_settlement_invoices)
lg stock adjust — Record a stock count correction (sayım düzeltme) or write-off (fire). mode=set sets the absolute on-hand at the slot; mode=delta applies a signed change (negative reduces). reason is required. (write · confirm + irreversible · shipper · API: create_stock_adjustment)
lg stock list — Read current on-hand stock levels. (read · — · shipper · API: check_stock_level)
lg stock list-movements — List the stock movements of this company, newest first: which product moved, in or out (or a transfer or a count adjustment), how much, in which warehouse and area, on which pallet, who performed it and when. (read · — · shipper · API: list_stock_movements)
lg stock move — Record a stock movement: inbound (giriş), outbound (çıkış), or transfer between locations. (write · confirm + irreversible · shipper · API: create_stock_movement)
lg tariff-chapters get — Answer "which chapter is this GTİP in / what does chapter 76 cover / what do the chapter notes say?". (read · — · all · API: get_tariff_chapter)
lg tariff-matrices get — FREE. (read · — · all · API: get_tariff_matrix)
lg tariff-matrices list — FREE. (read · — · all · API: list_tariff_matrix_runs)
lg tariff-matrices preview — FREE. (read · — · all · API: plan_tariff_matrix)
lg tariff-matrices run — Run an official duty + anti-dumping inquiry for ONE tariff code across MANY origin and destination countries at once, and record it. (write · confirm + irreversible · all · API: run_tariff_matrix)
lg tariffs coverage — FREE. (read · — · all · API: get_tariff_coverage)
lg tariffs extract — Read an invoice or proforma the user attached to THIS conversation and pull out its product lines with candidate GTİP/HS codes, plus the origin and destination it names. (write · — · all · API: extract_tariff_codes_from_invoice)
lg tariffs get — Read the result of a tariff inquiry that was already run: duty rate, the column it came from, the statutory rate, anti-dumping measures and the data notes. (read · — · all · API: get_tariff_inquiry)
lg tariffs inquiry — Start an official duty / anti-dumping inquiry for one GTİP/HS code and a COUNTRY PAIR (origin → destination). (write · confirm + irreversible · all · API: run_tariff_inquiry)
lg tariffs list — List this company's recent tariff inquiries (code, direction, counterpart country, status, date) so the user can find an earlier one by code or country instead of running a new inquiry. (read · — · all · API: list_tariff_inquiries)
lg tariffs search — Search the Turkish customs nomenclature (GTİP / HS) by goods description or by a partial code, and return matching codes with their official descriptions. (read · — · all · API: lookup_tariff_code)
lg teammates delete — Remove a person from the company team. (write · confirm + irreversible · all · API: remove_teammate)
lg teammates list — List the other users of this company (drivers excluded) with e-mail, active state and RBAC role (manager | employee | fleet-manager). (read · — · all · API: list_teammates)
lg teammates set-role — Change a team member's role (for example make them a manager, or move them back to employee). (write · confirm · all · API: update_teammate_role)
lg teammates set-status — Activate or deactivate a team member. (write · confirm · all · API: set_teammate_status)
lg tender-bids create — Submit YOUR bid for a tender using a file the user attached to THIS conversation (the filled specification spreadsheet, a PDF offer, or a zip). (write · confirm + irreversible · carrier · API: submit_tender_bid)
lg tender-bids get — Read YOUR OWN bid on a tender: its status (submitted, revision requested, accepted, rejected), when it was submitted and which files it carries. (read · — · carrier · API: get_my_tender_bid)
lg tender-bids set-status — As the tender owner, ACCEPT a carrier's bid, REJECT it, or ask for a REVISION. (write · confirm + irreversible · shipper · API: decide_tender_bid)
lg tenders create — Open a NEW document-based freight tender and generate its specification Excel from the lanes the user gives (origin → destination, vehicle/load type, monthly volume). (write · confirm · shipper · API: create_tender)
lg tenders generate — Generate a specification Excel for an EXISTING tender from the lanes/items the user gives and attach it to the tender as a document; returns a short-lived download link. (write · confirm · shipper · API: generate_tender_excel)
lg tenders get — Read one tender in full: specification text, dates, attached specification files (with short-lived download links) and — for the tender owner — every carrier bid with its status; a carrier sees only its own bid. (read · — · shipper, carrier · API: get_tender)
lg tenders list — List document-based freight tenders. (read · — · shipper, carrier · API: list_tenders)
lg trade-measures get — Answer "is there an anti-dumping / countervailing / safeguard measure on this GTİP code when importing INTO TURKEY?" straight from the Ministry of Trade's measures-in-force list, which we sync every day. (read · — · all · API: get_trade_measures)
lg trade-measures list — Browse Türkiye's trade-defence measures WITHOUT a GTİP code: by origin country ("which measures apply to goods from China"), by product ("which countries have a measure on aluminium foil"), by measure type, or by end… (read · — · all · API: list_trade_measures)
lg vehicle-assignments create — Hand a fleet vehicle over to a driver (open an assignment), so their expense receipts and trips are attached to the right tractor unit or trailer. (write · confirm · shipper, carrier · API: assign_vehicle_to_driver)
lg vehicle-assignments list — List which driver currently holds which vehicle (tractor unit / trailer). (read · — · shipper, carrier · API: list_vehicle_assignments)
lg vehicle-assignments revoke — Close an open vehicle assignment: the driver hands the vehicle back. (write · confirm · shipper, carrier · API: end_vehicle_assignment)
lg vehicle-expenses create — Record a cost against one of the company's own vehicles without an invoice file — category, amount and currency are required, the rest is optional. (write · confirm · shipper, carrier · API: create_vehicle_expense)
lg vehicle-expenses list — List the cost records of the company's own vehicles (maintenance, tyres, repair, roadworthiness test, insurance, fuel, tolls, parts) with a EUR total and a breakdown per category and per currency. (read · — · shipper, carrier · API: list_vehicle_expenses)
lg vehicles archive — Take a vehicle out of service (archive it) or put an archived one back into service. (write · confirm · shipper, carrier · API: archive_vehicle)
lg vehicles create — Add a vehicle to this company's fleet: plate plus its kind (truck, tractor unit, trailer, van, pickup, car), optionally brand, model, model year, VIN and a note. (write · confirm · shipper, carrier · API: create_vehicle)
lg vehicles get — Full detail of ONE fleet vehicle: identity (plate, kind, brand, model, model year, VIN), whether it is active or archived, ALL of its papers with their expiry date and days_to_expiry, how many papers expire soon or… (read · — · shipper, carrier · API: get_vehicle)
lg vehicles list — List this company's fleet vehicles with plate, kind (truck / tractor unit / trailer / van / pickup / car), brand, model, model year, VIN and how many of their papers (insurance, inspection, permits) expire soon or have… (read · — · shipper, carrier · API: list_vehicles)
lg vehicles search — Resolve one of this company's fleet vehicles (truck, tractor unit, trailer, van, pickup, car) to its vehicle_id by plate, brand, model or VIN. (read · — · shipper, carrier · API: lookup_vehicle)
lg vehicles update — Correct a fleet vehicle's fields: plate, kind, brand, model, model year, VIN or note. (write · confirm · shipper, carrier · API: update_vehicle)
lg warehouses create — Define a new warehouse for this company so stock can be held and dock slots booked there: a name and its street address are enough. (write · confirm · shipper · API: create_warehouse)
lg warehouses get — Read one warehouse in full: its identity and address, working days and hours, holiday opening, capacity, reservation slot rules (slot length, minimum and maximum duration), its safety and driver rules, the… (read · — · shipper · API: get_warehouse)
lg warehouses list — List the warehouses of this company: name, address, whether it is active, its working days and hours, open/closed area capacity, the dock slot length and how many areas (racks/bays) it has. (read · — · shipper · API: list_warehouses)
lg warehouses search — Resolve a warehouse by name to its warehouse_id, with its areas. (read · — · shipper · API: lookup_warehouse)
lg warehouses update — Update a warehouse: its name or address, working days and hours, holiday opening, capacity, dock slot rules, the driver/safety flags or the active flag. (write · confirm · shipper · API: update_warehouse)
ERP and CRM integration — Identity: no service accounts, a real user instead
An ERP or CRM connects to Logistivo as a real company user holding a personal access token minted from the panel. A synthetic service account — no company, exempt from scoping, nobody responsible for it — is never created.
This is architecture, not preference. Multi-tenant isolation rests entirely on the user's company; for an identity with no company the isolation scope becomes inert, and one wrong query on that token reads data that does not belong to it. The second reason is audit: the user column in the ledger ties "who ran this" to a person; a service account makes that column meaningless and leaves an invoice-issuing integration with no owner. The third is revocation: if a token belongs to a person, the integration falls silent when that person leaves. An ownerless bridge is the bridge that keeps writing quietly for years.
In practice: the company creates a normal user in the panel (for example "ERP Bridge", owned by a named employee), gives it a narrow role, mints a token from that user's session and puts it in the ERP.
That user is visible in the permission system, its rights can be trimmed, and its token can be revoked on its own. What is forbidden is not creating an account — it is creating an account that is not bound to a tenant.
One integration per token. If two systems share a token, revocation granularity is gone: cutting off the CRM means cutting off the ERP too.
ERP and CRM integration — Versioning and deprecation
The catalog response carries a contract_version integer; command rows carry since, deprecated_at and replaced_by. The contract version rises only on breaking changes.
The version does NOT rise for: a new command, a new optional parameter, a new field in a response. A consumer must ignore fields it does not know; a client that errors on an unknown field is non-compliant.
The version DOES rise for: removing or renaming a command, removing a response field, making an optional parameter required, narrowing an enum. The old shape stays alive for at least two calendar quarters (6 months) with deprecated_at and replaced_by.
Deprecation is announced in the catalog, not only in a changelog. Because the client draws itself from the catalog, a command with deprecated_at set prints a warning without any client release, and the integrator sees it in their logs.
The API name is frozen forever. The CLI identity (domain/verb) could be recut freely before it was announced; after announcement it changes only with a version bump and a migration window, during which the old pair is served as an alias.
In practice: the catalog response and the X-Contract-Version header carry the version; every command row carries since, deprecated_at, replaced_by and aliases (old domain/verb pairs). Calling a deprecated command still works and answers with an X-Deprecated: name; deprecated_at=…; replaced_by=… header plus a deprecated object in the body. ?domain= on the catalog resolves an old alias domain to the current command.
A client that understands version 1 and meets a larger number should log a warning and keep going; lg does exactly that. No command is deprecated today.
ERP and CRM integration — Stable identifiers: never hand an auto-increment id to your ERP as a key
Internal auto-increment ids are opaque and tenant-scoped: another company's valid id returns "not found" to you, not data. But being opaque does not make them business keys. What an ERP stores in its own record has to be a business key.
Load: code (the customer code, e.g. FSK2158). loads get already works by code.
Country: ISO 3166-1 alpha-2. countries search returns the code.
HS / tariff: the code itself — the nomenclature is already universal.
Invoice: the invoice number, once issued. A draft has only a surface id, and a draft id is NOT a business key — the ERP should carry its own reference and never persist a draft id.
Product, warehouse, contact: only a surface id today. The rule for new commands is explicit: any command returning a surface id also returns the tenant's own business key (SKU, code, tax number) so the ERP can match on its side.
ERP and CRM integration — Pagination, filtering and incremental sync
List commands take limit and return count. For a nightly sync the contract commits to the following; a list command that has adopted it advertises cursor and updated_since in its JSON Schema, and until it does, treat it as a small window (ceiling 20 rows, no cursor).
Request: limit (1..the per-command ceiling) plus cursor (opaque, forward-only, signed by the server). offset is NOT used — in a live table an offset shifts; a record inserted between two pages makes the ERP skip a row or count it twice, and it does so silently.
Response: { count, total?, next_cursor|null }. count is the number of rows in this response; when next_cursor is null the list is complete. Pass next_cursor back unchanged — it is not a page number and it is not yours to build.
A tampered or foreign cursor is rejected with 422 invalid_request; a cursor is bound to the company that received it and cannot page another tenant's data.
Filtering: the named parameters in the JSON Schema, combined with AND. There is no free query language — a DSL would be a second query surface we would have to defend.
Incremental sync: updated_since (ISO-8601, UTC) plus the cursor. Store the newest updated_at you have seen and pass it back next run; without it every ERP re-fetches everything every hour.
External reference: writing commands accept external_ref (your own document key), unique per company; a second create with the same external_ref is rejected rather than duplicated, and list/get return it so you can match records on your side.
ERP and CRM integration — Rate limits
The limit key is the token, not the IP: an ERP arrives from behind a single NAT, and an IP-based limit would count a whole company as one user. Two integrations with two keys never eat each other's quota — the second reason for "one integration per key".
Unauthenticated discovery (public/cli/catalog): 30 per minute. It is a dictionary, not a data source; read it once while writing your bridge, never in a production loop.
Catalog endpoints (GET common/commands, GET common/commands/{name}): 120 per minute per token. The catalog rarely changes and answers 304 to If-None-Match; do not re-fetch it before every command — lg caches it for an hour.
Read commands (POST common/commands/{name} with read_only: true): 120 per minute per token.
Writing commands (POST common/commands/{name} with read_only: false; an unknown name counts here too): 30 per minute per token. Writes have side effects; a runaway loop is billed.
AI or credit-consuming commands (tariffs inquiry, export-documents extract, export-documents generate): the real limit is CREDITS, not request count. A 200 response may have spent a credit.
429 always carries Retry-After and a body { error_code: "rate_limited", retry_after, limit, bucket }; honour it instead of inventing a fixed delay. The catalog publishes the current numbers under rate_limits.
ERP and CRM integration — Things we will not do
This is often the most useful part of a contract: knowing what will never arrive lets you design around it.
No service account and no principal that is not bound to a tenant.
No second path: there is no direct database access, no shell access and no bulk endpoint that bypasses the single enforcement point. The role gate, the confirmation gate, idempotency and the audit ledger are enforced only there.
Branching on error message text is not supported; the text is localised and changes without notice.
No offset pagination.
Using a surface id as a business key in your ERP is not supported.
Outgoing webhooks are not squeezed into this contract — but they now EXIST and are documented on this page (the webhook section above). The catalog is a PULL surface and the event stream is a SEPARATE contract: event names and the payload schema are versioned by their own rules and are not tied to the command catalog contract version.
The machine-readable catalog
This page was written for people. For an AI assistant or an automated client, the same information exists as a structured, unauthenticated copy.
GET /api/public/cli/catalog requires no authentication and returns the public identity of the commands: the frozen API name, the CLI domain/verb identity, the description, the JSON Schema parameters, and the read_only, confirmation_needed and irreversible flags. It returns no personal data, no tenant data and no sample records — only what the surface is.
The right way for an external agent to use it: read the catalog, explain in YOUR OWN words which jobs are possible, and when execution is needed, point the user at running it in their own environment with their own token. The agent never asks for, generates or relays a user's token.
Connecting Logistivo to an AI assistant for signup and freight requests is a separate channel with its own page:
The catalog is unauthenticated; execution is not. There is no way to run a command on an agent's behalf — what runs is always the user's own identity.
A Logistivo password, verification code or access token never passes through a chat. Do not build a flow that asks for, generates or relays one.
Whether a command is irreversible is written in the catalog. Never suggest "let's try it" on a command with irreversible: true.
Credit-consuming commands (tariffs inquiry, export-documents extract, export-documents generate) have a monetary result; do not call them in an automated loop.
A plain-text twin of this page lives at /cli.md; use it if you parse markdown more reliably than HTML.
The machine-readable definition: OpenAPI 3.1 and Postman
The catalog above is Logistivo's own format. The same information is also published in two industry-standard formats your tools already understand: an OpenAPI 3.1 definition and a Postman collection. Neither is written by hand — both are generated from the live command registry on every request, so they cannot go stale.
The definition IS the catalog, not a second reality. Every command appears as a POST /api/common/commands/ operation; the body schema is the command's own JSON Schema (byte for byte what the assistant sees), the response schema is the shared envelope, the gate flags (read_only, confirmation_needed, irreversible and the credit badge) sit in the x-logistivo extension, and the business error keys sit under x-error-keys. A retired command is marked deprecated: true and stays callable through its six-month window.
Alongside the command surface, the REST endpoints that carry the integration contract are in the definition too: 10 list endpoints with incremental sync (updated_since + cursor + limit) and 7 write endpoints that accept your own key (external_ref). Logistivo's REST surface is far larger than that; the rest is left out ON PURPOSE. Those payloads are the product's own contract, and a schema written from guesswork would break your generated client on the first call — and you would look for the bug in your code, not ours.
The definition is generated for the ROLE of whoever asks: another role's command never enters the document, because a definition is a contract rather than a dictionary and everything inside it must be callable with that token. The unauthenticated version is the union of the three roles, and there each operation states which roles see it under x-logistivo.roles.
Do not take our word for it — verify: npx @redocly/cli lint logistivo-openapi.json passes with zero warnings.
GET /api/common/spec/openapi.json — The OpenAPI 3.1 definition for your token's role. Carries an ETag; If-None-Match gives you a 304, so you are not re-downloading it on every build.
GET /api/common/spec/postman.json — The same scope as a Postman v2.1 collection, with base_url and token variables and folders already grouped by command domain.
GET /api/public/cli/openapi.json — The unauthenticated version — the union of the three roles. For designing and costing the bridge before you have a token.
GET /api/public/cli/postman.json — The unauthenticated Postman collection: one file you can hand to your team.
246 operations — 207 commands + 35 REST and control-plane operations + 4 catalog and identity endpoints. The numbers on this page are not typed by hand; they are counted from the live document on every request, so the day a command ships they go up here too — and they add up.
The security scheme — bearerAuth: a Passport token issued for a real user. A read-scoped key may call only operations flagged x-logistivo.read_only: true; a write attempt returns 403 BEFORE the confirmation gate, so a read-only key never sees a 409.
Every error branch is written out — Each operation describes 401/403/404/409/422/429/500 separately. The two meanings of 409 (confirmation required / idempotency conflict) and the two meanings of 422 (schema error / business rejection) are distinct in the document, so your generator does not collapse them into one branch.
No timestamp — The document carries no generation time. The same catalog produces the same bytes, which is why the ETag actually works and a definition you commit to version control does not look 'changed' on every download.
We did not invent product payloads — On REST write endpoints only the contract field (external_ref) is described; the rest of the body stays open as additionalProperties: true and the reader is pointed at the product documentation. A definition that is incomplete but true beats one that is complete but invented.
The per-operation error-key list is NOT exhaustive — x-error-keys lists only the keys that follow with certainty from the command's own record, and says so with x-error-keys-exhaustive: false. The full dictionary is at the root of the document; treat any key you do not recognise as a generic failure. A fabricated 'complete list' would have pushed you into never writing the default branch.
Discovery tools are not in the definition — search_commands, run_command and list_capabilities exist only inside the chat loop and cannot be called over HTTP. Had they been included, your generator would have written three methods that could never work.
Code generation: a typed client from the definition
You have the definition; now generate working code from it. `lg codegen` reads the OpenAPI document and writes a single-file, dependency-free client — TypeScript (type declarations plus a thin call wrapper) or PHP (a client class with PHPDoc array shapes). No `npm install`, no composer: `fetch` on the TypeScript side, `ext-curl` on the PHP side.
Generation happens on YOUR machine, not our server. That is not a preference: pin the definition in your repository once (`lg spec --out=...`) and the same command runs on every build with no network and no token, so your CI never depends on our uptime. And since the generated code is code that RUNS in your ERP, keeping it in your repository rather than pulling it from an API means every change shows up in `git diff`.
The generated client handles all three Logistivo rails correctly, and it learns which is which from the document rather than guessing: every operation declares its rail in `x-logistivo.surface`. The command rail unwraps the `{success, data:{…}}` envelope; a REST sync list is NOT enveloped and is returned as-is (unwrapping it would drop `next_cursor`); a REST write returns the endpoint's own product body. A general-purpose generator does not know this distinction — ours does.
The types are not guesswork: each command's argument interface is generated from the very JSON Schema the assistant sees. Required stays required, enums become union types, nested objects become nested types. In practice: if the server requires a field, the call that forgets it does not fail with a 422 in production — it fails at compile time.
No generation timestamp is written into the file: the same definition with the same flags produces the same bytes. Regeneration therefore makes no noise in `git diff`, and the one thing that genuinely changed — the contract — stays visible.
1. Pin the definition — The definition is generated for your role; every operation in it is callable with that token. Commit the file. lg login --api-key= lg spec --out=logistivo-openapi.json
2. Generate the client — One file comes out, carrying EVERY operation in the definition you pinned as a method: the commands your role can call, the REST endpoints that carry the integration contract, and the discovery endpoints. The count is YOUR role's, not the platform total — today a shipper key yields 185 command methods, a carrier 181, a customs broker 122. Generate from the unauthenticated public definition and the ceiling is 246 operations. lg codegen ts --from=logistivo-openapi.json --out=logistivo-client.ts lg codegen php --from=logistivo-openapi.json --out=LogistivoClient.php --namespace="Acme\\Logistivo" # or straight from the server, without downloading first: lg codegen ts --out=logistivo-client.ts
3. Verify, then use — If the generated code does not compile, the fault is ours, not yours: the integrity of the definition is guarded by a runnable check on the server. npx tsc --noEmit --strict --target ES2022 --module ESNext --moduleResolution bundler logistivo-client.ts php -l LogistivoClient.php
One method per operation — Every command in the document, every REST endpoint that carries the integration contract, and the discovery endpoints. Generate from your role's definition and you get only what you can call (185 commands for a shipper); generate from the public definition and you get all 246 operations. The day a command is added on the server, regenerating brings it in; there is no client release to wait for.
Argument types — Each command's `args` schema becomes an interface (an array shape in PHP). Required fields stay required, enums become unions. In TypeScript, forgetting a required field is a compile error; in PHP, your IDE and PHPStan see the same shape.
Error classes — The transport taxonomy (`error_code`) and the business taxonomy (`error_key`) are separate fields; the confirmation gate and the rate limit are separate error types. The error-key union stays open to unknown values — when the server declares a new key your code does not stop compiling, it falls into the `default` branch.
A correct retry policy — Automatic retries apply only to safe calls: reads, and writes that carry an idempotency key. A write without a key that times out does not mean 'nothing was written', so the client never blindly repeats it. On a 429 it waits the server's `Retry-After` instead of inventing a delay.
Result bodies are not typed — Command results come back as `unknown` (`mixed` in PHP), for the same reason as in the definition itself: result shapes are the command's business output, not part of the contract. A guessed result type would start lying the first time a field moved.
The generated file is not hand-edited — It is an output, not a source. If you need extra behaviour, wrap the generated class; anything you write inside it is lost on the next generation.
Discovery tools are not generated — `search_commands`, `run_command` and `list_capabilities` live only inside the chat loop and cannot be called over HTTP; they are absent from the definition and therefore from the client. Including them would have meant writing three methods that never work.
Honest scope
We would rather grow this section than shrink it. The value of an integration contract lies less in what it promises than in how clearly it states what it does not.
The CLI runs in a tenant user's context — Every command runs under the company and role of the user whose token is presented. A platform-operator mode — an administrative mode that reads across companies — is outside this surface and will not be opened from here.
Your command list depends on your role — A shipper account sees 185 commands, a carrier 181, a customs broker 122. If a command is missing from your catalog, the problem is the role, not the token. Driver accounts have no access to this surface at all.
Some commands spend credits — Tariff inquiries, field extraction from documents and document generation draw on your plan's credit balance. The catalog does not carry a credit cost field today; the information lives in the command description and adding the field is planned.
The catalog does not replace the existing REST API — The REST surface that powers the Logistivo web and mobile apps stays where it is and remains under the backwards-compatibility rule. The command catalog does not replace resource CRUD; it is the VERBS layer — the single, audited way to do a thing.
List commands return small windows today — Until a list command advertises cursor and updated_since in its schema, its ceiling is 20 rows with no cursor. It is not designed for extracting a full copy of your data; incremental sync is rolling out command by command.
Frequently asked questions
What is the Logistivo CLI used for?
It runs the operational work in your Logistivo account — listing and reading loads, seeing open freight demands, placing bids, moving a load's status forward, assigning drivers, preparing export documents, recording stock movements, issuing invoices, looking up HS tariffs and anti-dumping duties — from a terminal or a script. The same commands are callable over HTTP for ERP/CRM integrations.
How are commands named in the Logistivo CLI?
The shape is lg --parameter=value. The domain is a plural entity (loads, demands, invoices, export-documents) and the verb comes from a closed vocabulary (list, get, create, search, set, issue, generate and so on). Every verb of one entity lives in one domain, and the (domain, verb) pair is unique across the whole catalog.
How do I authenticate to the CLI?
With a personal access token minted from the Logistivo panel. lg login asks for the token and writes it to a local config file; in scripted environments set the LOGISTIVO_TOKEN environment variable instead. Over HTTP the token travels in the Authorization: Bearer header.
Can I create a service account for my ERP integration?
No. An integration connects with the token of a real company user created in the panel. A synthetic identity with no company makes multi-tenant isolation inert, makes the "who ran this" column in the audit ledger meaningless, and leaves a bridge that does not fall silent when its owner leaves. The right pattern is a dedicated user with a narrow role and a named owner.
Does Logistivo have a test (sandbox) environment?
Yes, and it is self-service: a separate sandbox tenant is opened for your company — same code, same command catalog, its own credit ledger. The environment is derived from the key's company and cannot be supplied with the request; a call made with the wrong profile stops server-side with 403 ENVIRONMENT_MISMATCH. A demand you open in the sandbox never reaches real carriers, a sandbox event is never delivered to your live webhook subscription, and your live balance does not move. Endpoints: POST /api/common/integration-sandbox (open), POST /api/common/integration-sandbox/keys (key), GET /api/common/integration-sandbox/go-live (go-live checklist). On the lg side, lg --sandbox and lg --live are single-run profile selectors.
How do I read the lg --json output in a script?
The output is the whole HTTP envelope: { "success": bool, "data": { "contract_version", "command", "status", "result" }, "code", "message" }. The business payload sits under .data.result; on list commands the rows are .data.result., the count is .data.result.count and the next page is .data.result.next_cursor. So the correct jq path looks like lg loads list --json | jq -r '.data.result.loads[].load_code'. A path that skips the envelope does not error, it silently returns null — in a watchdog script that becomes an alarm that never stops firing. The single exception is /api/public/status, whose body is deliberately envelope-free.
If I send the same request twice, do I get duplicate records?
Not if you sent an idempotency_key. A second call with the same key does not re-run the command; it returns the first result with status replayed. The key should be a UUID and must stay the SAME across retries — generating a fresh key per attempt is the same as not using idempotency.
What does --yes do?
It passes the confirmation gate. When a gated command is called without --yes nothing changes: the server summarises what it is about to do in one sentence, says whether it is irreversible, and stops (HTTP 409, CLI exit code 4). Repeating the command with --yes performs the work.
How do I detect a failed command in a script?
Read the exit code, never the message text: 0 success, 1 business rule rejection, 2 command or argument error, 3 authentication or role error, 4 confirmation required. Over HTTP the equivalents are 200, 422, 404, 403 and 409, and the error_code field in the body is stable. The error MESSAGE is localised and changes without notice.
How do I discover the available commands programmatically?
GET /api/public/cli/catalog for unauthenticated discovery, and GET /api/common/commands for the list your account can actually see. Both return each command's JSON Schema parameters; draw your client from them rather than hard-coding a command list.
Do I have to update my client when a new command is added?
No. The client pulls its command list from the catalog, so a command added on the server appears without a new client release. An update is needed only for a breaking change that raises the contract version, and in that case the old shape stays alive for at least six months.
Does the CLI replace the existing REST API?
No. The REST surface that powers the web and mobile apps stays in place. The command catalog does not replace resource CRUD; it is the verbs layer — the single, role-gated, confirmation-gated and audited way to perform an action.
How do I get a Logistivo API key and what can it do?
A manager with team-management permission issues it in the panel under Settings › Integration keys. The key acts on behalf of a real team member; the read-only scope opens the catalog and non-writing commands, the read+write scope opens every command (the confirmation gate stays as it is). It is shown once and not stored on the server; it can be rotated and revoked. When the member it acts for leaves, the key closes too. There is no service account.
What happens if I reuse an idempotency key for a different command?
The server answers 409 idempotency_conflict and names the command the key was first used for; nothing runs and nothing is replayed. Keys are scoped to your company, bound to one command and locked for 24 hours — after that the same key starts a new execution.
How are rate limits applied to my integration?
Per token, per minute, in three buckets: catalog reads 120, read commands 120, writing commands 30. A 429 always carries Retry-After and a body with retry_after, limit and bucket; wait that long and retry. The catalog publishes the current numbers under rate_limits.
How do I tell a business rejection from a transport error?
Branch on error_code first: failed (422) is a business rejection and carries a stable error_key such as INSUFFICIENT_CREDITS or NOT_FOUND; server_error (5xx) and network errors are retried with the same idempotency key; rate_limited (429) is waited out. Never branch on the error text.
Does Logistivo send webhooks? For which events?
Yes. You register an https address and pick the events you care about; a signed POST arrives when they happen. 13 events are subscribable today: load created/status changed/driver assigned, demand created and status changed, bid placed/revised/accepted, invoice draft/issued/cancelled, and warehouse reservation created/status changed. Every name is backed by an event class that is actually dispatched — nothing is announced and then never fired, because such an event is a silent failure the integrator waits for forever. Subscriptions are created under Settings › Integration › Webhooks, or from a terminal with lg webhooks create.
How do I verify a Logistivo webhook signature?
Every request carries an X-Logistivo-Signature header: t=,v1=. The signed string is ".", the algorithm is HMAC-SHA256 and the key is the subscription secret. Four rules: (1) verify the RAW bytes — decoding and re-serialising the body (JSON.stringify / json_encode) turns the same data into different bytes and the signature stops matching; this is the single most common mistake; (2) check the timestamp and drop anything where |now − t| > 300 s even if the signature is correct; (3) compare in constant time with hash_equals / timingSafeEqual; (4) deduplicate on event_id, not on the delivery id — event_id is preserved on replay while the delivery id changes. Working Node and PHP receivers live in examples/webhook-receiver/ in the repository and test themselves with --selftest.
What happens if a webhook cannot be delivered? Do I lose the event?
No. If your receiver does not return 2xx the delivery enters the retry schedule: 60 · 300 · 1800 · 7200 · 21600 seconds, 6 attempts in total, a window of roughly 8.6 hours. EVERY failure except 410 Gone is retried, 4xx included — losing a business event costs more than a few extra requests. After 10 consecutive exhausted deliveries the subscription is muted and the reason is recorded, so "why is nothing arriving" stays answerable. The delivery ledger keeps every attempt (status code, duration, error) and you can re-send a single delivery from the panel or with lg webhooks replay. Incremental sync (updated_since + cursor) is still there as the safety net.
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.