# 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 — and every write action in the panel now has a command, so your scripts and your own AI agents can do the work your team does on screen. Payments, legal consent and account closure deliberately stay in the browser.

This page is a contract. The command table below is not hand-written; it is generated from the live command registry on every request.

Canonical HTML: https://logistivo.com/en/developers/cli (Turkish: https://logistivo.com/tr/developers/cli)
Contract version: 1
Platform status: https://logistivo.com/api/public/status (JSON, no auth) · https://logistivo.com/en/status (page)

## What is the Logistivo CLI?

The Logistivo CLI (lg) is a command line client that runs the work of your Logistivo account — loads and trips, demands, bids and tenders, fleet and drivers, documents, contacts and current accounts, invoices and accounting, warehouse and stock, team members and customs tariffs — from a terminal, a script or your own AI agent. 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 — and through the MCP channel external AI assistants connect to.

- One command shape: lg <domain> <verb> --parameter=value. The catalog holds 519 commands across 125 domains today; the widest list a single role sees is 452 commands across 112 domains.
- Every write action in the panel has a command — measured against the panel's write endpoints, 100% of the in-scope ones are covered. Payments (3D Secure), legal consent and account closure deliberately stay in the browser; for those a command returns a link instead.
- Commands that take a document or a photo work outside the browser too: write --attachment_uuid=@./cmr.pdf and lg uploads the file first; over HTTP you upload to /api/common/commands:upload and pass the returned attachment_uuid.
- Integration keys carry an access level: employee (the default for new keys) for day-to-day operations, manager for team, settings, keys and money. A key never has more rights than the person it acts for.
- 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.

- [LIVE] 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.
- [LIVE] 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.
- [LIVE] 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.
- [LIVE] Panel coverage: every write action has a command — 519 commands across 125 domains, checked against the panel's write endpoints: every in-scope endpoint is backed by a command (a mirror of the endpoint or the same shared service), and none is unproven. Deliberately out of scope: payments with 3D Secure, legal consent, account closure and sign-in, which stay in the browser; key, webhook and sandbox management, which are lg built-in commands; and the driver app's own endpoints.
- [LIVE] 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. A preflight never enters the command's handler; when it shipped, 178 preflights across every write command in the catalog at the time produced not one write statement.
- [LIVE] Integration keys — API keys that act on behalf of a real team member, issued in the panel or with lg keys create: read/write scope, an access level (employee by default, or manager), lifetime, last use, rotate, transfer and revoke. lg login --api-key, lg whoami and a server-side-revoking lg logout are built on them.
- [LIVE] 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.
- [LIVE] 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.
- [LIVE] 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.
- [LIVE] Commands that take files — 33 commands take a document or a photo — a CMR, an invoice, a vehicle registration, pallet photos. Upload once to POST /api/common/commands:upload, or let lg do it with --attachment_uuid=@./file.pdf, and pass the returned attachment_uuid; MCP clients use the upload_attachment tool. The file type is detected from the content, and an upload stays usable for 24 hours.
- [LIVE] The lg terminal client — Published on npm under the MIT licence: install with `npm install -g @lgstv/lg`, or run it without installing via `npx @lgstv/lg`. It is a single dependency-free file, and because the HTTP surface is open you can integrate without it. It signs in with an approval code you confirm in the browser and then stays signed in, renewing its session by itself; it uploads local files for you (@path, --file, lg upload), manages integration keys (lg keys), never takes a secret on the command line, honours Retry-After, retries 5xx with the same idempotency key and warns on deprecated commands and newer contract versions.
- [ROLLING] 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.

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.

## 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.

### What that buys you in practice

- **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 452 commands, a shipper 448, a customs broker 312, and an affiliate (referral partner) account a set of its own, 55 commands.
- **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 every verb that acts on a load: create, list, get, update, set-status, assign-driver, list-problems, archive and more. 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.

## Coverage: everything in the panel, from a command

Many teams wire Logistivo into their own AI agent or automation harness, and an agent is only useful if it can finish the job. So the rule is simple: whatever an office user can change in the panel, a command can change too — behind the same role gate, the same confirmation gate and the same audit ledger.

This is measured, not asserted. Every write endpoint behind the panel is checked against the command registry, and an endpoint counts as covered only when a command declares it as its HTTP mirror or calls the same shared service. Of the in-scope write endpoints, 100% are covered and none is unproven. Every endpoint left out carries a written reason — identity and payment steps, the integration control plane, the driver app's own endpoints, client telemetry, dead copies with no caller — and the ones that matter to an integrator are listed below.

Commands that take a file fall under the same rule: 33 commands in the table take a document or a photo, and all of them work from lg, over HTTP and over MCP (see Commands that take files).

Management work is covered too, behind a stronger key: 62 commands in the table — team members and roles, company settings, bank accounts, saved cards, integration keys — need a manager-level integration key. An employee-level key, the default, runs day-to-day operations and is refused those with INSUFFICIENT_KEY_LEVEL.

Affiliate (referral partner) accounts have a catalog of their own: 55 commands across 20 domains — referrals, leads, CRM notes and tasks, sales decks, earnings and payouts, plus the account and team commands every role has. The affiliate-only commands are not in the public table below, which lists the three panel roles; an affiliate account sees them in GET /api/common/commands and in lg commands. A company that is both an affiliate and a shipper, carrier or customs broker sees both sets.

An agent should start with lg users get (API name get_current_user): it returns the role, the access level of the key in use, the effective permissions, and whether a pending legal approval or an unverified phone number is currently blocking writes — so the agent can predict a refusal instead of collecting it.

### When the answer is a link: handing a step to the browser

**1. Ask for the link** — Work that must happen in the person's own browser session is not a command, but the command surface knows about it. lg handoff-links generate (API name get_browser_handoff_link) returns a logistivo.com link for the purposes your role allows: package_purchase, payment_cards, legal_documents, integration_keys, affiliate_demo_accounts and account_close. It is free, writes nothing, and the link carries no secret — the person signs in as usual.

    lg users get --json
    lg packages list
    lg handoff-links generate --purpose=package_purchase --package_slug=<slug> --billing_period=yearly
    lg handoff-links generate --purpose=legal_documents

**2. Hand it to the person** — Show the link to the user or send it through your own channel. They finish the step there: the contract approval and 3D Secure payment for a package, adding a card, approving pending legal texts, issuing an API key whose secret is shown once, or closing their own account.

**3. Read the outcome with a command** — Once the step is done, its result is visible to commands again: the package and credit balance, the saved card (masked, last four digits only), the new key in the key list. An agent never takes a card number, a payment or a consent in a chat.

    lg packages get
    lg payment-cards list
    lg users get

### What is deliberately not a command — and what to do instead

- **Payments and 3D Secure** — Buying or upgrading a package and adding a card need 3D Secure in the browser. Commands list packages and prices, read the balance and its history, list saved cards (masked), set the default card and remove one; the purchase itself goes through the handoff link.
- **Legal consent** — Approving legal texts is done by the person, in the browser. lg users get tells you that an approval is pending; no command can give it.
- **Account closure, sign-in and password** — Closing your own account, signing in and changing a password are identity steps and stay in the browser. lg user-accounts preview shows what closing the account would mean, without doing it.
- **Integration keys, webhooks and the sandbox** — Managing them returns secrets that must be shown once, in your terminal, so they are lg built-in commands (lg keys, lg webhooks, lg sandbox) over their own HTTP endpoints rather than catalog commands. The catalog carries only the two secret-free key commands: integration-keys list and integration-keys revoke.
- **The dashboard assistant chat** — Integration keys cannot drive the AI assistant chat inside the dashboard; starting or continuing a run with a key answers 403. An agent connects through the command endpoints, lg or the MCP channel instead — there each command runs at the key's access level and its writes are recorded as integration writes.
- **The driver app** — Driver accounts have no command catalog, and the driver app keeps its own endpoints. Office users manage drivers, their documents, expenses and trips through commands.

Coverage is measured by a report that compares the command registry with the panel's write endpoints; a panel endpoint added without a command shows up in it as unproven. If you find a panel action you cannot reach from a command, report it to support.

## Install and identity

lg is a single-file, dependency-free Node script (Node 18+), published on npm as @lgstv/lg. The package installs two identical commands, lg and logistivo — use logistivo if lg is already an alias on your machine. Because it is one file, copying lg.mjs onto a customer's ERP server also works. You do not need the client to build an ERP/CRM integration — the same commands are available over HTTP.

### 1. Install

Node 18 or newer is all you need; there are no other dependencies. Install it globally or run it without installing via npx.

    npm install -g @lgstv/lg
    lg --version
    
    # without installing:
    npx @lgstv/lg --version

### 2. Sign in with an approval code

lg login shows a short approval code and opens logistivo.com/connect in your browser; over SSH, in CI or with --no-browser it only prints the address, which you can open on any device, your phone included. Sign in there as usual, check that the code on the page is the one in your terminal, choose read-only or read-and-act access and approve. Nothing secret is typed into the terminal: no password, no key, no one-time code. The session then stays signed in on that computer — closing the terminal does not sign you out, and lg renews the access token by itself before it expires. lg logout ends the session on the server as well, and you can remove it in the panel under Settings › Integration keys › AI and CLI connections. lg login --password (email and password) remains as a fallback. Requires lg 0.11 or later.

    lg login
    # Server: https://logistivo.com
    # Sign in with your Logistivo account in the browser and approve this code:
    #   Approval code: BCDF-GHJK
    #   Address:       https://logistivo.com/connect
    # Check that the code in the browser is the SAME. Waiting for approval… (Ctrl+C to cancel)
    # Signed in (approval code). The session stays signed in on this computer and renews itself.
    # 452 commands · 112 domains ready — list them with 'lg commands'.

### 3. For servers and CI: a key, not an approval code

A server or a CI job has nobody to approve a code in a browser. Issue a key in the panel (Settings › Integration keys) or with lg keys create — 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.

    export LOGISTIVO_TOKEN="..."
    lg loads list --status=in_transport --limit=5

### 4. Verify and explore the catalog

lg commands lists every command your role can see, grouped by domain; lg help <domain> prints that domain's verbs, and lg help <domain> <verb> 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.

    lg commands --domain=loads
    lg help loads
    lg help loads create

### Environment variables

- `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.

Never put the key into version control, a CI log or a chat window. If you signed in with an approval code or an integration key, lg logout ends it on the server too; in a password session (lg login --password) 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 ***, and every parameter a command declares secret.

## 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

- `lg` — The client.
- `loads` — The domain — the entity you are working on, plural.
- `list` — The verb — what you do to that entity, drawn from a closed vocabulary that grows only by deliberate decision: simple verbs (list, get, create, update, delete, search, preview, generate, extract …) and compound verbs that name what is attached or changed (add-document, set-status, assign-driver). lg help <domain> lists a domain's verbs.
- `--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 stated in each command's parameter description — typically 20 on a plain list call and up to 100 in sync mode (with cursor or updated_since).
- `--json` — Raw JSON output.

### Global flags valid on every command

- `--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=<uuid>` — 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.
- `--dry-run` — Runs a preflight instead of the command: validation, the gates and the credit calculation run, nothing is written (see the preflight section). Valid on every command and on lg batch.
- `--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.
- `--profile=<name>, --sandbox, --live` — Which profile to run this one call as. --sandbox and --live pick the profile that holds a sandbox or a live key; the environment is derived from the key on the server, so a mismatch stops with 403 ENVIRONMENT_MISMATCH instead of writing to the wrong place.
- `--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.

Flag names are the schema's parameter names, and lg accepts both spellings: --load_code and --load-code reach the same field (lg help prints the kebab-case form). Seeing a flag therefore tells you which JSON field it lands in, and moving from the CLI to HTTP needs 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. On a command that takes a file, put @ in front of a local path (--attachment_uuid=@./cmr.pdf) — see Commands that take files.

## 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).

### Reading: lists and single records

**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

    {
      "ok": true,
      "load": {
        "load_code": "FSK2158",
        "status": "in_transport",
        "load_type": "Komple",
        "load_date": "2026-08-19",
        "delivery_date": "2026-08-29",
        "weight_kg": 18400,
        "from": "Gebze, Türkiye",
        "to": "Duisburg, Germany",
        "carrier": "...",
        "driver": null,
        "external_ref": "ORD-2026-0917",
        "updated_at": "2026-08-19T12:10:00Z"
      }
    }

**Open document-consistency flags**

    lg loads list-problems --only_open=true --limit=10

List commands return compact rows; call get with a code for the full picture of one load. `load_type` and `status` are NOT the same kind of field: `status` is a stable key (pending · in_transport · transit · done), while `load_type` is a localised label ("Komple", "Parsiyel") — never branch on its text.

### Search first, then filter

**Resolve the country**

    lg countries search --query=Germany --json

    {
      "ok": true,
      "count": 1,
      "matches": [
        { "country_id": 57, "iso_code": "DE", "name": "Almanya", "name_en": "Germany" }
      ]
    }

**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"

Country, product, warehouse, contact, driver and load-type parameters take a surface id. You resolve that id with the matching search command. countries search also returns the ISO 3166-1 alpha-2 code — that, not our incrementing id, is the stable key to store on the ERP side.

### Wiring it into a script

**Turn loads waiting to be picked up into a tab-separated list**

    lg loads list --status=pending --limit=20 --json \
      | jq -r '.data.result.loads[] | [.load_code, .load_date, .from, .to] | @tsv'

    FSK2158	2026-09-02	Gebze, Türkiye	Duisburg, Germany
    FSK2160	2026-09-03	İzmir, Türkiye	Trieste, Italy

**Feed expiring fleet documents to a watchdog script**

    lg fleet-documents list --days=30 --scope=both --include_expired=false --json \
      | jq -e '.data.result.count == 0' > /dev/null || echo "Documents are expiring"

**Branch on the exit code, never on the message**

    if ! lg loads get --load_code="$CODE" --json > load.json; then
      case $? in
        2) echo "Wrong command name or argument" ;;
        3) echo "Invalid token or role not permitted" ;;
        4) echo "Confirmation required" ;;
        *) echo "Business rule rejected it" ;;
      esac
    fi

The --json output is the HTTP envelope itself: {success, data:{…}} — the business payload sits under .data.result (on list commands .data.result.<entity>, .data.result.count, .data.result.next_cursor). The field names are stable; this is the only correct entry point for a shell script. A jq path that skips the envelope does not error, it SILENTLY returns null — in a watchdog script that means an alarm that never stops firing.

### Writing and confirming

**Unconfirmed call — you get a preview, no work is done (carrier role only)**

    lg bids create --demand_code=TLP-2026-0417 \
      --amount_per_vehicle=1450 --currency_code=EUR --vehicle_count=2

    Confirmation required (exit code 4)
      About to bid: TLP-2026-0417 · 1,450.00 EUR per vehicle · 2 vehicles
      This action cannot be undone.
      Repeat the same command with --yes to confirm.

**Confirmed call (carrier role only)**

    lg bids create --demand_code=TLP-2026-0417 \
      --amount_per_vehicle=1450 --currency_code=EUR --vehicle_count=2 \
      --idempotency-key=$(uuidgen) --yes

**Move a load forward (carrier role only)**

    lg loads set-status --load_code=FSK2158 --status=transit --date=2026-08-21 --yes

When a gated command is called without --yes nothing changes: the server summarises what it is about to do and stops.

### Export documents: draft, fill, generate

**Draft**

    lg export-documents create --doc_type=proforma-invoice --currency_code=EUR --json

**Add an item**

    lg export-documents add-item --document_id=8412 \
      --item='{"description":"Aluminium profile","quantity":120,"unit_price":18.40}'

**Generate it (gated)**

    lg export-documents generate --document_id=8412 --yes

The document flow is multi-step on purpose. Creating a draft and adding items are ungated; only the final step — generating the document — asks for confirmation, because that is the irreversible one.

## Without the client: the HTTP command API

lg is a convenience, not the door. Catalog and execution both go through three HTTP endpoints (files have a fourth, the upload endpoint — see Commands that take files); 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.

- `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, credit_cost, since, deprecated_at, replaced_by, aliases and the access fields key_level, permission, file_params, secret_parameters and cli_only; at the top level the error_keys dictionary, rate_limits, idempotency_ttl_hours, your catalog roles, the attachments (upload) contract and the access level of the key you are calling with. 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.

Every response carries X-Contract-Version; a 304 or 429 without a body still tells you which contract you are talking to.

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.

**Fetch the catalog**

    curl -s https://logistivo.com/api/common/commands \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" \
      -H "Accept: application/json"

**Run a read command**

    curl -s -X POST https://logistivo.com/api/common/commands/list_loads \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"args":{"status":"in_transport","limit":20}}'

**A writing command — confirmation and idempotency together**

    curl -s -X POST https://logistivo.com/api/common/commands/create_demand_bid \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
            "args": { "demand_code": "TLP-2026-0417",
                      "amount_per_vehicle": 1450,
                      "currency_code": "EUR",
                      "vehicle_count": 2 },
            "confirm": true,
            "idempotency_key": "9f1c6e2a-4b7d-4a51-9d3e-0c8b2f5a7e10"
          }'

**The response to an unconfirmed call: 409, with a human summary in the body**

    HTTP/1.1 409 Conflict

    {
      "error_code": "needs_confirmation",
      "command": "create_demand_bid",
      "confirmation": {
        "summary": "About to bid: TLP-2026-0417 · 1,450.00 EUR per vehicle · 2 vehicles",
        "irreversible": true
      }
    }

## Commands that take files

33 commands in the table take a document or a photo: attach a CMR to a load, read HS codes from an invoice, create a vehicle from its registration document, estimate a pallet from photos, upload a company document, cross-check an invoice against a packing list. In the browser you drop the file into a form; everywhere else it takes two steps — upload the file, then pass its id to the command.

A parameter takes a file when its name ends in attachment_uuid or attachment_uuids — photos[].attachment_uuid inside a list, front_attachment_uuid on a cheque. You do not have to remember that: every catalog row carries file_params with the exact paths, the schema marks those fields (format: uuid, x-logistivo-file: true), and the catalog response carries an attachments block with the upload address, the accepted types, the size limits and the time to live. lg help <domain> <verb> shows them too.

Uploading is a write, so a read-scoped key cannot upload; any write-scoped key can, at either access level. Uploads belong to the user, not to the key: two keys of the same user see the same uploads, and nobody can use someone else's. An id that is unknown, expired or not yours is simply not found, and the reason is not disclosed.

Dashboard chat attachments and command uploads are separate spaces: a file attached in the assistant chat cannot be passed to a command by id, and a command upload never appears in a chat thread.

### Three ways to pass a file

**1. lg: put @ in front of a local path** — lg uploads the file first, then runs the command with the returned id; a value without @ is the id of a file you uploaded earlier. Lists take comma-separated paths or a repeated flag, nested fields take JSON, and --file <path> is a shortcut when the command has exactly one top-level file field. lg verifies each file locally (exists, size, type) before anything is sent, and lg batch uploads every distinct file once before the batch runs.

    lg load-documents create --load_code=FSK2158 --attachment_uuid=@./cmr-4471.pdf --yes
    lg tariffs extract --file ./invoice.pdf
    lg document-checks run --attachment_uuids=@./invoice.pdf,@./packing-list.pdf
    lg equipment-inspections create --event_type=pickup --plate=34ABC123 \
      --photos='{"attachment_uuid":"@./front.jpg","angle":"front"}'
    
    # upload only, keep the id for later
    lg upload ./invoice.pdf --json

**2. HTTP: upload, then pass attachment_uuid** — Send the file as multipart/form-data in a field named file. A new upload answers 201; the same file uploaded again by the same user answers 200 with status replayed and the same id, and nothing is written. As everywhere on this page, the example shows the data object inside the envelope.

    curl -sS -X POST https://logistivo.com/api/common/commands:upload \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json" \
      -F "file=@./cmr-4471.pdf"
    
    {
      "contract_version": 1,
      "command": "upload_attachment",
      "status": "ok",
      "attachment": {
        "attachment_uuid": "3f0c9b7e-5d2a-4c8e-9a41-7b2e6d1f0a93",
        "filename": "cmr-4471.pdf",
        "mime_type": "application/pdf",
        "size_bytes": 182344,
        "sha256": "…",
        "extraction_status": "done",
        "expires_at": "2026-10-13T09:12:44+03:00",
        "reused": false
      }
    }
    
    curl -sS -X POST https://logistivo.com/api/common/commands/attach_chat_file_to_load \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Content-Type: application/json" \
      -d '{"args":{"load_code":"FSK2158","attachment_uuid":"3f0c9b7e-5d2a-4c8e-9a41-7b2e6d1f0a93"},"confirm":true}'

**3. MCP: the upload_attachment tool** — An MCP client cannot send multipart, so the channel offers one extra tool that is not in the catalog: upload_attachment takes a filename and the content as base64 and returns the same attachment_uuid. It calls the same service as the HTTP upload, under the same rules.

    → tools/call upload_attachment
      { "filename": "cmr-4471.pdf", "content_base64": "JVBERi0xLjcK…" }
    ← { "command": "upload_attachment", "status": "ok",
        "attachment": { "attachment_uuid": "3f0c9b7e-5d2a-4c8e-9a41-7b2e6d1f0a93", … } }
    
    → tools/call attach_chat_file_to_load
      { "load_code": "FSK2158", "attachment_uuid": "3f0c9b7e-5d2a-4c8e-9a41-7b2e6d1f0a93" }

### Rules

- **The type comes from the content** — The file name and the declared type are ignored; the stored extension is the detected one. Accepted for upload: jpg, jpeg, png, webp, heic, heif, pdf, xlsx, xls, csv, txt, doc, docx, xml, zip. Each command then applies its own, narrower list before anything runs and answers UNSUPPORTED_FILE_TYPE with the accepted types, or FILE_TOO_LARGE.
- **Size** — Images up to 10 MB, other files up to 15 MB.
- **Time to live** — An upload can be used for 24 hours. After that it is no longer resolved (ATTACHMENT_NOT_FOUND) — it is not deleted. Upload again if a command fails for that reason.
- **Same file, same id** — Uploading the same file again (same name, size and content) returns the existing id instead of writing a new one, as long as it has at least an hour left, and it does not use up your daily ceiling.
- **Daily ceilings** — 300 uploads per user per day (TOO_MANY_FILES) and 2 GB per company per day (DAILY_LIMIT_REACHED); both answer 429 with retry_after.

### Endpoint

- `POST https://logistivo.com/api/common/commands:upload` — Multipart upload in the field file. 201 for a new upload, 200 with status replayed for the same file. Returns data.attachment: attachment_uuid, filename, mime_type, size_bytes, sha256, extraction_status, expires_at and reused.

An upload is not a command and has no business effect on its own: nothing happens to your data until a command uses the id. That is also why uploading never asks for confirmation — the command that consumes the file carries the gate.

## 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?

Some actions are not behind this gate because they are not commands at all: buying a package, adding a card with 3D Secure, approving legal texts and closing an account happen in the person's own browser. For those, lg handoff-links generate returns a logistivo.com link — free, writing nothing, carrying no secret — and the person finishes the step there. No flag, however it is set, turns a payment or a consent into an API call.

- **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.

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.

## 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 519 commands today; 233 of them only read and write nothing. Of the remaining 286 write commands, 251 are gated: called without confirm they already answer 409 and describe what they would do. The gap was the other 35 — 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 structural before it is a measurement: the preflight path never enters the command's handler, so it holds for every command, including the ones added later. It was also measured when it shipped: every SQL statement issued while preflighting every write command in the catalog at the time (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.

```bash
curl -sS https://logistivo.com/api/common/commands/create_invoice_draft \
  -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H 'Content-Type: application/json' \
  -d '{"dry_run":true,"args":{"recipient_contact_id":25457,"currency_code":"EUR","customer_id":4711}}'

{
  "status": "dry_run", "dry_run": true, "result": null,
  "would_execute": true, "would_write": true,
  "would_need_confirmation": false,
  "would_charge": { "credits": 0 },
  "would_stop": null,
  "arguments": {
    "accepted": ["recipient_contact_id", "currency_code"],
    "missing_required": [],
    "ignored": ["customer_id"],
    "type_mismatches": []
  },
  "guarantees": {
    "wrote_nothing": true, "charged_credits": 0,
    "consumed_idempotency_key": false, "recorded_execution": false
  }
}
```

- **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.

### From the terminal

```bash
# One command
lg invoices issue --invoice-id=208 --dry-run

# A batch file (JSONL)
lg batch nightly.jsonl --dry-run
lg batch nightly.jsonl --dry-run --offline   # local line/name check only, no server round trip
```

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.

## Connecting with an API key

An ERP, CRM, script or AI agent connects to Logistivo with an integration key issued in the panel or with lg keys create. The key acts on behalf of a real team member: that person's role decides the command list, the key's access level decides how heavy the work may be, 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 the access level, 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, just as it ends an approval-code session there (only the password-session lg logout cannot do that).

    lg login --api-key -  < key.txt
    lg whoami
    lg loads list --limit=5

### 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.

    curl -s https://logistivo.com/api/common/commands \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json"
    
    curl -s https://logistivo.com/api/common/integration-keys/current \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json"

### 4. Or manage keys from the terminal: lg keys

lg keys is the terminal twin of the panel screen: same endpoints, same gate (the team-management permission, and a manager-level key when you call it with a key). create, rotate and transfer return a new token once: with --save-profile=NAME it goes straight into a local profile and is never printed; otherwise lg prints it once. In --json mode the token is included only if you add --show-secret, and a --json call with neither flag is refused before the request is sent, so the token cannot silently land in a script's output. rotate, transfer and revoke ask for confirmation (--yes); a key is addressed by its exact name or its uuid, never by a guess. The access level carries over unchanged to a rotated or transferred key.

    lg keys list
    lg keys create --name="ERP bridge" --scope=write --access-level=employee --save-profile=erp
    lg keys update "ERP bridge" --access-level=manager
    lg keys rotate "ERP bridge" --save-profile=erp --yes
    lg keys transfer "ERP bridge" --to=<user_id> --yes
    lg keys revoke "ERP bridge" --yes
    lg whoami        # user, company, role, scope and the key's access level

### Two scopes

- `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.

### Two access levels

- `employee` — The default for every new key. Day-to-day operations: loads, demands, bids, trips, fleet and fleet documents, documents, contacts — every command that needs no management permission, plus the fleet permissions. A command that needs more answers 422 with error_key INSUFFICIENT_KEY_LEVEL and required_key_level: manager; the matching REST endpoint answers 403 with the same key. The right level for an ERP nightly sync, a telematics bridge or an operations script.
- `manager` — Everything the person the key acts for may do, management included: team members and roles, company settings, integration keys, bank accounts and their verification, saved cards, company relationships, affiliate payouts. A manager key never adds a right the person does not have — without the team-management permission it still cannot manage the team. Keys that existed before access levels were introduced are manager keys, so they behave exactly as before.

### Lifecycle

- 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, transferring and revoking is open to managers with team-management permission — the same gate as adding or removing team members — and, when done with an integration key, that key must be manager-level. A manager can issue a key on behalf of another member; a dedicated, narrowly-scoped member is recommended for a bridge.

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). Scope, domain grants and access level are three independent axes and all of them must pass: a read key cannot write even at manager level, an employee write key cannot manage the team, and a loads:write domain grant does not open management work. Issue the key on behalf of the least-privileged member that covers your needs: the role decides which records you can see at all.

## 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.

### 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. The third axis is the access level: employee (the default) runs day-to-day operations, manager also does team, settings, keys and money work. Choose employee unless the bridge does management work.

### 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 the access level, 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.

    export LOGISTIVO_TOKEN=<the token you copied from the panel>

### 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.

    curl -s https://logistivo.com/api/common/integration-keys/current \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json"
    
    curl -s -X POST https://logistivo.com/api/common/commands/list_loads \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" \
      -H "Accept: application/json" -H "Content-Type: application/json" \
      -d '{"args":{"limit":5}}'
    
    lg logs --outcome=denied

### 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.

    // Node 18+
    const parts = Object.fromEntries(header.split(',').map(p => p.trim().split('=')));
    if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return res.status(400).end();
    const expected = createHmac('sha256', SECRET).update(`${parts.t}.${rawBody}`).digest('hex');
    const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? '');
    if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(400).end();

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.

## 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, 448 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.

### Four steps

**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.

    curl -s -X POST https://logistivo.com/api/common/integration-sandbox \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json"
    
    curl -s https://logistivo.com/api/common/integration-sandbox \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json"

**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.

    curl -s -X POST https://logistivo.com/api/common/integration-sandbox/keys \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Content-Type: application/json" \
      -d '{"name":"ERP bridge — rehearsal","scope":"write","expires_in_days":30}'
    
    lg profile add sandbox --api-key -  < sandbox.txt
    lg --sandbox whoami        # environment: SANDBOX — your writes are not real
    lg --live    whoami        # environment: LIVE — your writes are real

**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.

    KEY=$(uuidgen)
    lg --sandbox contacts create --legal_name="Weber GmbH" --country_code=DE \
      --idempotency-key=$KEY --yes
    lg --sandbox contacts create --legal_name="Weber GmbH" --country_code=DE \
      --idempotency-key=$KEY --yes --json | jq '.data.status'    # "replayed"
    lg --sandbox loads list --json | jq '.data.result.count'

**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.

    curl -s https://logistivo.com/api/common/integration-sandbox/go-live \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Accept: application/json" \
      | jq '{ready: .data.ready, items: [.data.items[] | {key, status}], scope: .data.scope_proposal.grants}'

### What does NOT leak into production — and how

- **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, 448 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.

### Endpoints

- `GET https://logistivo.com/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 https://logistivo.com/api/common/integration-sandbox` — Opens the sandbox (one per company, idempotent). Requires the team.manage permission.
- `POST https://logistivo.com/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 https://logistivo.com/api/common/integration-sandbox/go-live` — The go-live checklist and a scope proposal — computed from your real traffic in the sandbox.
- `POST https://logistivo.com/api/common/integration-sandbox/renew` — Opens a new sandbox GENERATION and retires the previous one.

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.

## 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.

    lg profile add erp --api-key -
    lg profile use erp
    lg --profile=erp loads list --limit=5

### 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.

    lg sync --entities=loads,contacts --out=./sync
    lg sync --entities=loads --full
    lg sync --list

### 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.

    lg completion bash >> ~/.bashrc
    lg completion zsh >> ~/.zshrc

### Secret parameters never go on the command line

A few commands take a secret — connecting the Paraşüt accounting integration, for example, takes that system's client credentials and password. lg never accepts such a value as a flag, because it would end up in your shell history and in ps output: it asks for it without echo, or reads it from --<field>-file=PATH or from stdin (--<field>-file=-), and keeps it in memory only. A command whose required parameter is secret runs only from lg: called from the chat, over MCP or inside lg batch it is refused with SECRET_REQUIRES_CLI. The audit ledger always records the value as ***.

    lg parasut-settings update --parasut-company-id=123456 --username=ops@example.com --yes
    # client_id, client_secret and password are asked for without echo
    
    # non-interactive: read each secret from a file (or - for stdin)
    lg parasut-settings update --parasut-company-id=123456 --username=ops@example.com \
      --client-id-file=./id.txt --client-secret-file=./secret.txt --password-file=./password.txt --yes

## 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, the read_only, confirmation_needed and irreversible flags, the credit_cost, and the access fields key_level, permission, file_params, secret_parameters and cli_only. 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.

    https://logistivo.com/api/public/cli/catalog

    curl -s https://logistivo.com/api/public/cli/catalog | jq '.data.commands[] | select(.domain=="loads")'

    {
      "name": "list_loads",
      "domain": "loads",
      "verb": "list",
      "read_only": true,
      "confirmation_needed": false,
      "irreversible": false,
      "parameters": { "type": "object", "properties": { "status": { "...": "..." } } }
    }

### Rules for external assistants

- 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.
- Commands whose credit_cost is not zero (tariffs inquiry and export-documents generate, for example) have a monetary result; do not call them in an automated loop.
- Payments, card numbers and legal consent never go through a chat or an agent. Offer the link from handoff-links generate instead; the person finishes the step in their own browser.
- An agent acting with a user's key should call users get first: it returns the role, the key's access level, the effective permissions and whether a pending consent or phone verification is blocking writes — predict refusals instead of collecting them.
- A plain-text twin of this page lives at /cli.md; use it if you parse markdown more reliably than HTML.

Connecting Logistivo to an AI assistant for signup and freight requests is a separate channel with its own page: https://logistivo.com/en/mcp

## 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/<name> 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.

- `GET https://logistivo.com/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 https://logistivo.com/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 https://logistivo.com/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 https://logistivo.com/api/public/cli/postman.json` — The unauthenticated Postman collection: one file you can hand to your team.

### Pull it with lg

    lg spec --format=openapi --out=logistivo-openapi.json
    lg spec postman            # the default filename comes from the server: logistivo-<role>-postman-collection.json
    lg spec --public --json | jq '.paths | keys | length'

### Or without a token, in one line

    curl -s https://logistivo.com/api/public/cli/openapi.json -o logistivo-openapi.json
    npx @openapitools/openapi-generator-cli generate -i logistivo-openapi.json -g typescript-fetch -o ./sdk

### What is inside the definition

- 559 operations: 519 commands + 36 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.

### What we deliberately did not do

- 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.

Do not take our word for it — verify: npx @redocly/cli lint logistivo-openapi.json passes with zero warnings.

## 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.

### Three commands

**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=<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 448 command methods, a carrier 452, a customs broker 312. Generate from the unauthenticated public definition and the ceiling is 559 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

### Using it

    import { LogistivoClient, LogistivoError } from "./logistivo-client.js";
    
    const client = new LogistivoClient({ token: process.env.LOGISTIVO_TOKEN });
    
    // Command rail — the envelope is already unwrapped
    const loads = await client.listLoads({ limit: 20 });
    console.log(loads.status, loads.result);          // "ok" | "replayed"
    
    // REST sync rail — cursor pagination, whole list in one call
    const rows = await client.collect((cursor) =>
      client.restCommonLoadsList(cursor ? { cursor, limit: 100 } : { updated_since: "1970-01-01T00:00:00Z", limit: 100 }));
    
    // The confirmation gate is its own error type; show the summary to a human
    try {
      await client.issueInvoice({ invoice_id: 42 });
    } catch (e) {
      if (e instanceof LogistivoError && e.errorCode === "needs_confirmation") { /* show the summary */ }
    }

### What is in the generated file

- 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 (448 commands for a shipper); generate from the public definition and you get all 559 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.

### What we deliberately left out

- 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.

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.

## 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.

    POST /api/common/commands/issue_invoice
    {
      "args": { "invoice_id": 10488 },
      "confirm": true,
      "idempotency_key": "9f1c6e2a-4b7d-4a51-9d3e-0c8b2f5a7e10"
    }

- **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}.

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.

## 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.

### Two parameters, two response fields

- `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.

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.

    # First page
    curl -sS -X POST https://logistivo.com/api/common/commands/list_loads \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Content-Type: application/json" \
      -d '{"args":{"updated_since":"2026-09-01T00:00:00Z","limit":100}}'
    
    # Next page — no need to resend updated_since, the cursor carries the position
    curl -sS -X POST https://logistivo.com/api/common/commands/list_loads \
      -H "Authorization: Bearer $LOGISTIVO_TOKEN" -H "Content-Type: application/json" \
      -d '{"args":{"cursor":"eyJ1IjoiMjAyNi0wOS0wMiAwNzowOToyNiIsImkiOjEyMzQ1Li4u"}}'
    
    {
      "status": "ok",
      "command": "list_loads",
      "result": {
        "ok": true,
        "count": 100,
        "total": 298,
        "truncated": true,
        "loads": [
          { "load_code": "FSK2158", "external_ref": "ORD-2026-0917",
            "status": "in_transport", "updated_at": "2026-09-02T04:09:26Z" }
        ],
        "next_cursor": "eyJ1IjoiMjAyNi0wOS0wMiAwNzowOToyNiIsImkiOjEyMzQ1Li4u"
      }
    }

Same thing with lg: lg loads list --updated_since=2026-09-01T00:00:00Z --limit=100 --json, then lg loads list --cursor=<next_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.

### 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.

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.

## 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.

### 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.

- `load.created` (load, since 2026-09-02) — A load became visible to your company (created by you or shared with you).
- `load.status_changed` (load, since 2026-09-02) — A load moved to a new status (pending, in_transport, transit, done).
- `load.driver_assigned` (load, since 2026-09-02) — A driver was assigned to a load.
- `demand.created` (demand, since 2026-09-02) — A freight demand was created by your company.
- `demand.status_changed` (demand, since 2026-09-02) — A freight demand changed status.
- `demand_bid.created` (demand_bid, since 2026-09-02) — A bid was placed on a demand you are party to.
- `demand_bid.revised` (demand_bid, since 2026-09-02) — A bid was revised (a new revision replaced the previous one).
- `demand_bid.accepted` (demand_bid, since 2026-09-02) — A bid was accepted.
- `invoice.created` (invoice, since 2026-09-02) — An invoice draft was created.
- `invoice.issued` (invoice, since 2026-09-02) — An invoice was issued (it now carries an invoice number).
- `invoice.cancelled` (invoice, since 2026-09-02) — An invoice was cancelled.
- `warehouse_reservation.created` (warehouse, since 2026-09-02) — A warehouse dock/area reservation was created.
- `warehouse_reservation.status_changed` (warehouse, since 2026-09-02) — A warehouse reservation was approved, rejected or otherwise moved.

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.

### Whose events you hear

A subscription belongs to a tenant, and the gate has two stages. A cheap pre-filter first works out which companies could hear the event; then — BEFORE any delivery row is created — the record is re-fetched with exactly the same visibility query the PANEL uses. If the record does not come back from that query, the payload is empty and no delivery is ever created.

- Putting the gate a second time, with the real query, removes the entire class of "the webhook showed me something I cannot see in the panel". The pre-filter can be wrong: a pivot may be stale, sharing may have been switched off.
- Draft records never reach a webhook — they are not in the panel either. If we announced them, the integrator would get a 404 when it went to fetch the record.
- The admin "visible to everyone" flag on a load produces NO candidates: fanning an event out to every subscriber on the platform because of that flag would leave the promise "a subscriber receives its own tenant's events" at the mercy of a flag.

### Verifying the signature

Every request carries an X-Logistivo-Signature header: t=<unix>,v1=<hex>. The signed string is "<t>.<RAW BODY>" 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.

    POST /logistivo/webhook HTTP/1.1
    Content-Type: application/json
    User-Agent: Logistivo-Webhook/1
    X-Logistivo-Event: load.status_changed
    X-Logistivo-Event-Id: 337bcb06-6a3f-5b13-9afe-ffd32b0e326b
    X-Logistivo-Delivery: 0f2a…
    X-Logistivo-Webhook: ad19a183-…
    X-Logistivo-Attempt: 1
    X-Logistivo-Signature: t=1788356998,v1=ceb4eb…
    X-Contract-Version: 1
    
    {
      "id": "0f2a…",
      "event": "load.status_changed",
      "event_id": "337bcb06-…",
      "occurred_at": "2026-09-02T13:09:58+00:00",
      "api_version": 1,
      "company_id": 123094,
      "object": "load",
      "data": { "id": 18069, "code": "FUN8530", "external_ref": null, "status": "in_transport" },
      "context": { "status": "in_transport" }
    }

- 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.

- Timeouts: 8 s total, 4 s to connect — a slow receiver must not occupy a worker. Queue the work and return 2xx immediately.
- Retry schedule: 60 · 300 · 1800 · 7200 · 21600 seconds → 6 attempts, a window of roughly 8.6 hours.
- 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.

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.

### The address gate — why we do not accept every URL

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:

- https only, ports 443/8443 only, embedded credentials (https://user:pass@…) rejected;
- 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.

### Endpoints

- `GET https://logistivo.com/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 https://logistivo.com/api/common/integration-webhooks` — 201 + { webhook, secret, shown_once: true }. The secret appears only here.
- `PATCH https://logistivo.com/api/common/integration-webhooks/{uuid}` — Name / address / events / active flag. Does NOT touch the secret.
- `DELETE https://logistivo.com/api/common/integration-webhooks/{uuid}` — Removes it: the row is soft-deleted (history stays queryable) and pending deliveries are cancelled.
- `POST https://logistivo.com/api/common/integration-webhooks/{uuid}/rotate-secret` — 201 + a new secret. The old value stops working at that moment.
- `POST https://logistivo.com/api/common/integration-webhooks/{uuid}/test` — A ping event — real signature, real path, real retry schedule.
- `GET https://logistivo.com/api/common/integration-webhooks/{uuid}/deliveries` — The most recent deliveries: status, attempt number, HTTP code, duration, error tag.
- `POST https://logistivo.com/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.

All of them sit on the same chain as the key ledger and require the team.manage permission. Another tenant's subscription id returns 404 — "does not exist" and "is not yours" are not separated; separating them would let someone confirm that a foreign id exists. A replay opens a new delivery row and copies the payload VERBATIM; sending the record's current state would be rewriting history.

### The signing secret

- 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.

### From a terminal: lg webhooks

The whole subscription is managed from the command line, without opening the panel. Event names, limits and header names are not written into the client — all three come from the server's own ledger, exactly like the command catalog.

    lg webhooks events                                   # subscribable events (live catalog)
    lg webhooks create --name="ERP bridge" \
      --url="https://erp.example.com/logistivo/webhook" \
      --events=load.created,load.status_changed,invoice.issued
    lg webhooks test "ERP bridge"                        # ping — real signature, real path
    lg webhooks tail "ERP bridge"                        # stream deliveries (Ctrl-C to stop)
    lg webhooks deliveries "ERP bridge" --status=failed
    lg webhooks replay <delivery-uuid>
    lg webhooks rotate-secret "ERP bridge" --yes

lg webhooks tail polls the delivery ledger at a sane interval (there is no separate streaming endpoint on the server) and prints not only NEW rows but also the status/attempt CHANGE of a row it already knows — watching a delivery enter the retry schedule after a 5xx and finally succeed is exactly what you want to see. If more deliveries happen between two polls than fit on a page, that does not stay silent: a warning is printed.

### Working receiver examples

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.

    node receiver.mjs --selftest     # 14/14
    php  receiver.php --selftest     # 14/14
    
    LOGISTIVO_WEBHOOK_SECRET=whsec_… node receiver.mjs --port 8787 --state ./seen.json

## 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.

    lg status
    
    Platform  OPERATIONAL   api operational · database operational · commands operational · queue operational · storage operational
    
    Bridge: ERP nightly sync  ● FAILING — 6 failed requests in a row · most frequent: INSUFFICIENT_SCOPE (forbidden) ×6
      last successful call   2026-09-03T01:12:44+03:00  (3 h 34 m ago)
      last call (any result) 2026-09-03T04:41:02+03:00  (0 m ago)
      last 7 days            1482 calls · 41 errors (2.8%) · 6 rejected at the gate
      last 24 hours          212 requests · 206 ok / 6 denied / 0 client / 0 server · streak 6
      webhook                2 subscriptions (2 open / 0 muted) · last 24 h: 40/40 delivered
      expiry                 42 days left
    
    # every key in the company, worst first
    lg status --all
    
    # as a watchdog: exit code 1 if the bridge is failing, 6 if the platform is down
    lg status --json || raise-alert

### Five badges, all of them measured

- **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.

### Rules we do not bend

- 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.

### Unauthenticated status endpoint

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.

    curl -sS https://logistivo.com/api/public/status
    
    {
      "status": "operational",
      "components": {
        "api": "operational",
        "database": "operational",
        "commands": "operational",
        "queue": "operational",
        "storage": "operational"
      },
      "checked_at": "2026-09-03T04:39:38+03:00"
    }

- 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.

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.

## 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.

- ok → HTTP 200, error_code `—`, CLI exit 0. —
- replayed → HTTP 200, error_code `—`, CLI exit 0. Idempotent replay; the work did NOT run again.
- failed → HTTP 422, error_code `failed`, CLI exit 1. No — a business rule rejected it (see error_key). Pointless until the argument changes.
- needs_confirmation → HTTP 409, error_code `needs_confirmation`, CLI exit 4. Yes, with confirm: true.
- idempotency_conflict → HTTP 409, error_code `idempotency_conflict`, CLI exit 2. No — the key was first used for a different command (conflicting_command). Mint a new key.
- forbidden → HTTP 403, error_code `forbidden`, CLI exit 3. No — role gate, or a read-scoped key on a writing command.
- not_found → HTTP 404, error_code `not_found`, CLI exit 2. No — wrong or removed command name.
- (schema) → HTTP 422, error_code `invalid_request`, CLI exit 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 exit 6. Yes — exponential backoff with the SAME idempotency key; the key was not locked.

### Transport layer

- `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.

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.

### Business error keys (error_key)

The dictionary is generated from the same source the catalog publishes; a key is stable once listed, and removing one raises the contract version. A few commands surface the class name of an unexpected exception as the key — those values are not part of the contract.

The full dictionary is published with the catalog: https://logistivo.com/api/public/cli/catalog (field `error_keys`).

## 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.

### When the version increases, and when it does not
- **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.

### Why an announcement does not bump the version

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.

### The window: at least 6 months, and the old form KEEPS WORKING throughout

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.

### The only retirement in force today

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.

### What you can rely on
- 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.

### What you cannot rely on
- 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

Generated from the live command registry (519 commands).

### account-settings
- `lg account-settings get` (API name: `get_account_settings`) — read, gate: —, roles: shipper. Required: none. Read the account settings (Settings → Account): the company display name (and legal name, read-only), the signed-in user's own name and e-mail (e-mail is read-only here), the saved load code prefix and how the company… [manager key]
- `lg account-settings update` (API name: `update_account_settings`) — write, gate: confirm, roles: shipper. Required: none. Change account settings (Settings → Account): company_name (the company's display name, company-wide), first_name / last_name (the signed-in user's own name — when only one is given the other part is kept),… [manager key]

### accounting-settings
- `lg accounting-settings get` (API name: `get_accounting_settings`) — read, gate: —, roles: all. Required: none. 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…
- `lg accounting-settings set-status` (API name: `set_accounting_module`) — write, gate: confirm, roles: all. Required: enabled. Turn the company's accounting module on or off (muhasebe modülünü aç/kapa).
- `lg accounting-settings update` (API name: `update_accounting_settings`) — write, gate: confirm, roles: all. Required: none. 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…

### addresses
- `lg addresses list` (API name: `list_worked_addresses`) — read, gate: —, roles: all. Required: none. Loading and delivery addresses already used on this company's previous loads (newest first): address_id, name, street, city and country. query filters by name, street or city. limit 1-50 (default 20).
- `lg addresses search` (API name: `lookup_address`) — read, gate: —, roles: carrier. Required: query. Find a place for a route plan stop: the company's own address book first, then a geocoder (counted against the daily routing allowance) when the book has fewer than 3 hits.

### ai-settings
- `lg ai-settings get` (API name: `get_ai_settings`) — read, gate: —, roles: shipper. Required: none. Read our company's AI switches (Settings → AI), e.g. ai_file_classify ("AI document analysis"): whether files posted into our load chats are read by AI, sorted into document types and split into company documents, or… [manager key]
- `lg ai-settings update` (API name: `update_ai_settings`) — write, gate: confirm, roles: shipper. Required: settings. Turn our company's AI switches on or off (Settings → AI) — e.g. "stop AI from analysing the files in our load chats" → settings=[{"slug":"ai_file_classify","enabled":false}]. settings is a list of {slug, enabled}; slugs… [manager key]

### announcements
- `lg announcements list` (API name: `list_announcements`) — read, gate: —, roles: all. Required: none. Show the system announcements Logistivo sent to this user (new features, maintenance, policy changes) — newest first.
- `lg announcements mark-read` (API name: `mark_announcements_read`) — write, gate: —, roles: all. Required: none. Mark system announcements as read for this user.

### attachments
- `lg attachments get` (API name: `read_attachment`) — read, gate: —, roles: all. Required: attachment_uuid. Read a file the user attached to THIS conversation (listed in the user message as [Attachments] with their attachment_uuid). [takes a file: attachment_uuid]

### background-jobs
- `lg background-jobs get` (API name: `get_background_job`) — read, gate: —, roles: all. Required: job_id. Check a background job that another command queued and returned a job_id for (document consistency re-analysis, file classification, tariff inquiry, tariff invoice reading, receipt pack…): status pending / running /…

### bank-accounts
- `lg bank-accounts create` (API name: `create_company_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_id. Add one of YOUR OWN company bank accounts to the account book (the accounts you get paid into). [manager key]
- `lg bank-accounts delete` (API name: `delete_company_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id. Delete one of OUR OWN company bank accounts (for example a closed account). [manager key]
- `lg bank-accounts extract` (API name: `extract_supplier_bank_accounts`) — write, gate: confirm + irreversible, roles: all. Required: contact_id, attachment_uuid. Read a supplier's bank accounts (IBAN, bank, holder, currency) from one of ITS invoices and save them on that business contact. [takes a file: attachment_uuid]
- `lg bank-accounts list` (API name: `list_bank_accounts`) — read, gate: —, roles: all. Required: none. List bank accounts.
- `lg bank-accounts preview` (API name: `preview_bank_account_iban`) — read, gate: —, roles: all. Required: iban. Check an IBAN without saving anything: is it valid (country, length, checksum) and which bank it belongs to (detected offline from the bank code where known).
- `lg bank-accounts set-default` (API name: `set_default_company_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id. Make one of OUR OWN bank accounts the default — the IBAN printed on our invoices and e-invoices. [manager key]
- `lg bank-accounts set-status` (API name: `decide_bank_account_verification`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id, decision. Confirm or reject a bank account that was read from a document by AI or written by an integration — your own account or a business contact's account (find the ones waiting with list_bank_accounts… [manager key]
- `lg bank-accounts update` (API name: `update_company_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id. Update one of OUR OWN company bank accounts (the accounts customers pay us into): IBAN, bank, account holder, branch code, account number or currency. [manager key]

### banks
- `lg banks list` (API name: `list_banks`) — read, gate: —, roles: all. Required: none. List the banks of Turkey with their bank_id and SWIFT/BIC code — the bank_id that create_company_bank_account and update_company_bank_account need.

### bids
- `lg bids accept` (API name: `accept_demand_bid`) — write, gate: confirm + irreversible, roles: shipper. Required: bid_code. Accept a carrier's bid on one of this company's demands.
- `lg bids calculate` (API name: `calculate_bid_earnings`) — read, gate: —, roles: carrier. Required: demand_code, amount_per_vehicle, currency_code. Before bidding: what this carrier would earn and how many credits a bid would cost if the customer accepts it.
- `lg bids cancel` (API name: `cancel_demand_bid`) — write, gate: confirm + irreversible, roles: carrier. Required: bid_code. Withdraw (geri çek) one of THIS carrier company's PENDING bids.
- `lg bids create` (API name: `create_demand_bid`) — write, gate: confirm + irreversible, roles: carrier. Required: demand_code, amount_per_vehicle, currency_code. Submit a bid (teklif ver) on an open demand. amount_per_vehicle is the freight PER VEHICLE, not the total.
- `lg bids get` (API name: `get_my_demand_bid`) — read, gate: —, roles: carrier. Required: bid_code. Detail of ONE bid this carrier company submitted: demand, status (pending / accepted / revised / cancelled / price_too_high), price per vehicle and total, currency, vehicle count, customs included or customs price,…
- `lg bids list` (API name: `list_my_bids`) — read, gate: —, roles: carrier. Required: none. List the bids (teklif) THIS carrier company has submitted, newest first, with their outcome.
- `lg bids revise` (API name: `revise_demand_bid`) — write, gate: confirm + irreversible, roles: carrier. Required: bid_code, amount_per_vehicle. 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).

### carbon-reports
- `lg carbon-reports calculate` (API name: `calculate_load_carbon_emission`) — read, gate: —, roles: all. Required: none. Calculate the CO2 emission of an EXISTING load (load_code: weight and route from the load, including its Ro-Ro leg) OR of a route you describe (transport_mode road|roro + weight_kg + departure_address + arrival_address,…
- `lg carbon-reports download` (API name: `export_carbon_emission_reports`) — read, gate: —, roles: all. Required: none. Export ALL of the company's saved CO2 emission reports to one Excel file (one row per report: date, route, mode, weight, distance, emission, factor, method) and return a short-lived download link.
- `lg carbon-reports generate` (API name: `generate_carbon_emission_report`) — write, gate: confirm + irreversible, roles: all. Required: none. Produce the signed CO2 emission report (PDF, with QR code) for a load OR for a route, save it to the company's report list and return a download link.
- `lg carbon-reports get` (API name: `get_carbon_emission_report`) — read, gate: —, roles: all. Required: report_id. 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…
- `lg carbon-reports list` (API name: `list_carbon_emission_reports`) — read, gate: —, roles: all. Required: none. List the CO2 emission reports this company has generated (newest first): report number, file name, who created it and when.

### carrier-routes
- `lg carrier-routes get` (API name: `get_carrier_routes`) — read, gate: —, roles: carrier. Required: none. This carrier's route definitions (Rota Yönetimi): the countries served — always <country> ↔ Türkiye in both directions — and the vehicle types and load types operated, with ids and names.
- `lg carrier-routes set` (API name: `set_carrier_routes`) — write, gate: confirm, roles: carrier. Required: sending_country_ids, vehicle_type_ids, load_type_ids. REPLACE this carrier's route definitions (Rota Yönetimi) with the given FULL sets: countries served (each to/from Türkiye, both directions), vehicle types and load types. [manager key]

### chart-of-accounts
- `lg chart-of-accounts list` (API name: `list_chart_of_accounts`) — read, gate: —, roles: all. Required: none. 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.

### checks
- `lg checks assign-invoices` (API name: `attach_check_to_invoices`) — write, gate: confirm + irreversible, roles: all. Required: check_id, lines. Allocate a pending, field-confirmed cheque to one or more open LOCAL invoices of the same contact and currency: in = sales invoices (collection), out = purchase invoices (payment). [manager key]
- `lg checks bounce` (API name: `mark_check_bounced`) — write, gate: confirm + irreversible, roles: all. Required: check_id, reason. Record that a cheque BOUNCED (came back unpaid).
- `lg checks cancel` (API name: `cancel_settlement_check`) — write, gate: confirm + irreversible, roles: all. Required: check_id, reason. CANCEL a pending cheque (e.g. registered by mistake, returned before use): no journal entry is written and any invoice reservation is released. [manager key]
- `lg checks collect` (API name: `mark_check_collected`) — write, gate: confirm + irreversible, roles: all. Required: check_id. 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.
- `lg checks create` (API name: `create_settlement_check`) — write, gate: confirm, roles: all. Required: direction. Register a cheque in the cheque portfolio. [manager key] [takes a file: front_attachment_uuid, back_attachment_uuid]
- `lg checks get` (API name: `get_settlement_check`) — read, gate: —, roles: all. Required: check_id. 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,…
- `lg checks list` (API name: `list_settlement_checks`) — read, gate: —, roles: all. Required: none. 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…
- `lg checks remove-invoices` (API name: `detach_check_from_invoices`) — write, gate: confirm, roles: all. Required: check_id. Release a pending cheque's invoice allocation: the reservations are removed (invoices become payable again) and the cheque goes back to the unallocated pending cheques. [manager key]
- `lg checks run` (API name: `execute_settlement_check`) — write, gate: confirm + irreversible, roles: all. Required: check_id. EXECUTE a pending, allocated cheque against its invoices: the reservations become payments (the invoice balances close), and the notes receivable (121, received cheque) or notes payable (321, given cheque) journal… [manager key]
- `lg checks update` (API name: `update_settlement_check`) — write, gate: confirm, roles: all. Required: check_id, amount, currency_code, due_date. Correct a pending cheque's fields and CONFIRM them — the human sign-off on what AI read from the photo (or on a manual entry). [manager key]

### companies
- `lg companies add-role` (API name: `add_company_role`) — write, gate: confirm + irreversible, roles: all. Required: company_role. Add a role to this company (firmaya rol ekle): customer (shipper), carrier, customs (customs broker) or affiliate. [manager key]
- `lg companies delete-logo` (API name: `delete_company_logo`) — write, gate: confirm + irreversible, roles: all. Required: none. Remove this company's logo (firma logosu) from the branded login portal, documents and the panel. [manager key]
- `lg companies get` (API name: `get_company_profile`) — read, gate: —, roles: all. Required: none. This company's own profile (firma profili): legal name, tax number and tax office, address and postal code, city/country, payment term (vade), the company roles it has (customer/carrier/customs/affiliate) and the roles…
- `lg companies invite` (API name: `invite_company_to_platform`) — write, gate: confirm + irreversible, roles: all. Required: email, company_type. Invite a company that is NOT on Logistivo yet by e-mail (e.g. a carrier we want to give a load to).
- `lg companies list` (API name: `list_platform_companies`) — read, gate: —, roles: all. Required: company_type. Logistivo platform companies this company has already worked with on loads (shared load chats), by company_type: customer (shippers), carrier or customs (customs brokers).
- `lg companies search` (API name: `search_platform_companies`) — read, gate: —, roles: all. Required: none. Check whether a company is on Logistivo — by EXACTLY ONE of: tax_number (exact VKN / tax id), vat_number (exact EU/UK VAT number) or email (any e-mail of that company, matched by its domain; then company_type is…
- `lg companies set-brand-colors` (API name: `set_company_brand_colors`) — write, gate: confirm, roles: carrier, customs broker. Required: primary_color, secondary_color. Set the two colours of this company's branded login portal (markalı giriş portalı, /p/<slug>/login) — primary_color and secondary_color as #RRGGBB hex. [manager key]
- `lg companies set-logo` (API name: `set_company_logo`) — write, gate: confirm + irreversible, roles: all. Required: attachment_uuid. Make an uploaded image this company's logo (firma logosu) — shown on the branded login portal, documents and the panel. attachment_uuid is a JPEG or PNG image (max 8 MB) uploaded to this conversation or with `lg upload`. [manager key] [takes a file: attachment_uuid]
- `lg companies update` (API name: `update_company_profile`) — write, gate: confirm, roles: all. Required: none. Update this company's profile (firma profili) — send ONLY the fields that change: legal_name (ticaret unvanı), tax_number (VKN / VAT id), tax_branch (vergi dairesi), address_street, postal_code, city_id, country_code… [manager key]

### company-documents
- `lg company-documents create` (API name: `upload_company_document`) — write, gate: confirm, roles: all. Required: attachment_uuid, document_type_slug. File a document the user attached (in this chat, or uploaded to the command surface) as one of the company's own documents (firma evrakı yükle): tax certificate (vergi levhası), signature circulars (imza sirküleri),… [takes a file: attachment_uuid]
- `lg company-documents delete` (API name: `delete_company_document`) — write, gate: confirm + irreversible, roles: all. Required: document_id. Remove one of the company's own documents (tax certificate, signature circular, trade registry gazette, permits…) from its document list — e.g. a wrong or outdated upload. document_id is the one shown by…
- `lg company-documents list` (API name: `list_company_documents`) — read, gate: —, roles: all. Required: none. 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.
- `lg company-documents list-missing` (API name: `list_missing_company_documents`) — read, gate: —, roles: all. Required: none. Which company documents (tax certificate, signature circulars, trade registry gazette, TIO/L2/C2 permits …) this company is REQUIRED to have for its company type and country, which of them are on file and verified, and…

### company-domains
- `lg company-domains create` (API name: `create_company_domain`) — write, gate: confirm + irreversible, roles: all. Required: domain, verification_email. Add an e-mail domain to our company (e.g. firma.com.tr) and e-mail a 6-digit verification code to a mailbox ON that domain. [manager key]
- `lg company-domains delete` (API name: `delete_company_domain`) — write, gate: confirm + irreversible, roles: all. Required: company_domain_id. Remove one of our UNVERIFIED company domains (e.g. added by mistake). [manager key]
- `lg company-domains list` (API name: `list_company_domains`) — read, gate: —, roles: all. Required: none. List our company's e-mail domains (Company settings → Domains): each row has company_domain_id, domain, verified (true once the 6-digit code was entered) and, for unverified ones, the mailbox the last code was sent to.
- `lg company-domains resend` (API name: `resend_company_domain_code`) — write, gate: confirm + irreversible, roles: all. Required: company_domain_id. E-mail a NEW 6-digit verification code for one of our unverified company domains (the previous code stops working). [manager key]
- `lg company-domains verify` (API name: `verify_company_domain`) — write, gate: confirm + irreversible, roles: all. Required: company_domain_id, verification_code. Verify one of our unverified company domains with the 6-digit code that was e-mailed to the mailbox on that domain. [manager key]

### company-relationships
- `lg company-relationships accept` (API name: `accept_company_relationship`) — write, gate: confirm + irreversible, roles: all. Required: relationship_id. Accept a connection proposal another company sent us (relationship_id from list_company_relationships direction=incoming). [manager key]
- `lg company-relationships create` (API name: `create_company_relationship`) — write, gate: confirm + irreversible, roles: all. Required: contact_id, company_id. Send a connection proposal to link one of our business contacts with its company on the Logistivo platform (company_id from search_platform_companies). [manager key]
- `lg company-relationships list` (API name: `list_company_relationships`) — read, gate: —, roles: all. Required: none. List our company connections on the platform (direction=outgoing, default: the connections in our directory, with status and roles such as carrier/customs_broker/supplier) or the connection proposals other companies…
- `lg company-relationships reject` (API name: `reject_company_relationship`) — write, gate: confirm + irreversible, roles: all. Required: relationship_id. Reject a connection proposal another company sent us (relationship_id from list_company_relationships direction=incoming). [manager key]
- `lg company-relationships revoke` (API name: `revoke_company_relationship`) — write, gate: confirm + irreversible, roles: all. Required: relationship_id. End a company connection we are a party of (either side may end it), or withdraw a pending proposal. [manager key]

### contacts
- `lg contacts add-address` (API name: `add_business_contact_address`) — write, gate: confirm, roles: all. Required: contact_id, name, address, country_code. Add an address to a business contact (e.g. a warehouse or branch): a label, the full street address and the country (ISO-2 code); postal code optional.
- `lg contacts add-bank-account` (API name: `add_business_contact_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: contact_id, iban. Add a bank account (IBAN) to a business contact so it can be used on payment instructions and invoices.
- `lg contacts add-person` (API name: `add_business_contact_person`) — write, gate: confirm, roles: all. Required: contact_id, first_name, last_name, email. Add a person (yetkili kişi) to a business contact: first name, last name and e-mail are required; phone, phone country code, job title and a note are optional.
- `lg contacts archive` (API name: `archive_business_contact`) — write, gate: confirm, roles: all. Required: contact_id. Archive a business contact (cariyi arşivle): it disappears from lists and pickers and becomes read-only, but nothing is deleted — invoices, loads and bank accounts stay linked, and restore_business_contact brings it…
- `lg contacts create` (API name: `create_business_contact`) — write, gate: confirm, roles: all. Required: legal_name, country_code. Create a new business contact (cari) in the company directory.
- `lg contacts delete-address` (API name: `delete_business_contact_address`) — write, gate: confirm + irreversible, roles: all. Required: address_id. Delete one of a business contact's addresses.
- `lg contacts delete-bank-account` (API name: `delete_business_contact_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id. Delete one of a business contact's bank accounts (for example an old or wrong IBAN).
- `lg contacts delete-person` (API name: `delete_business_contact_person`) — write, gate: confirm + irreversible, roles: all. Required: person_id. Delete a person from a business contact, together with that person's notification preferences.
- `lg contacts extract` (API name: `extract_business_contact_from_image`) — read, gate: —, roles: all. Required: attachment_uuid. Read a business card, letterhead or invoice header (image or PDF) and return the company details on it — legal name, tax number and office, VAT number, e-mail, phone, IBAN (checksum-checked, bank detected), address,… [takes a file: attachment_uuid]
- `lg contacts extract-text` (API name: `extract_business_contacts_from_text`) — read, gate: —, roles: all. Required: record_kind, text. Read pasted text (Excel/CSV rows, e-mail signatures, a typed list) and return up to 100 draft records of ONE kind: companies, persons, addresses or bank_accounts.
- `lg contacts get` (API name: `get_business_contact`) — read, gate: —, roles: all. Required: none. 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.
- `lg contacts invite` (API name: `invite_business_contact`) — write, gate: confirm + irreversible, roles: all. Required: contact_id, email. Invite a business contact to join Logistivo by e-mail.
- `lg contacts list` (API name: `list_business_contacts`) — read, gate: —, roles: all. Required: none. List the company's business contacts (customers, carriers, suppliers, consignees) with optional text search over name, legal name and tax number.
- `lg contacts list-addresses` (API name: `list_business_contact_addresses`) — read, gate: —, roles: all. Required: contact_id. List the saved addresses of one business contact (label, street address, postal code, country) with address_id, needed by update/delete.
- `lg contacts list-person-notifications` (API name: `list_business_contact_person_notifications`) — read, gate: —, roles: all. Required: person_id. Show which load notifications a contact person receives and on which channel (e-mail, push, SMS), plus the notification types that can be chosen with their allowed channels — use those slugs with…
- `lg contacts list-persons` (API name: `list_business_contact_persons`) — read, gate: —, roles: all. Required: contact_id. List the people (yetkili kişiler) saved on one business contact: name, e-mail, phone, job title, note, and which load notifications each one receives.
- `lg contacts list-routes` (API name: `list_business_contact_routes`) — read, gate: —, roles: all. Required: contact_id. Show which countries a business contact that is linked to a carrier on the platform serves: its declared route pairs (from-country → to-country) and the distinct country list.
- `lg contacts remove-company-link` (API name: `unlink_business_contact_company`) — write, gate: confirm + irreversible, roles: all. Required: contact_id. Remove the link between a business contact and its company on the Logistivo platform. [manager key]
- `lg contacts restore` (API name: `restore_business_contact`) — write, gate: confirm, roles: all. Required: contact_id. Bring an archived business contact back (cariyi arşivden geri al): it returns to lists and pickers and can be edited again.
- `lg contacts search` (API name: `lookup_business_contact`) — read, gate: —, roles: all. Required: query. Resolve a business contact (customer/partner/recipient) by name, legal name or tax number to its contact_id.
- `lg contacts set-default-bank-account` (API name: `set_default_business_contact_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id. Make one of a business contact's bank accounts its DEFAULT account — the one pre-selected when you pay this contact.
- `lg contacts set-person-notifications` (API name: `set_business_contact_person_notifications`) — write, gate: confirm, roles: all. Required: person_id, preferences. Turn load notifications on or off for a contact person, per notification type and channel (e-mail, push, SMS).
- `lg contacts update` (API name: `update_business_contact`) — write, gate: confirm, roles: all. Required: contact_id. Update fields of an existing business contact.
- `lg contacts update-address` (API name: `update_business_contact_address`) — write, gate: confirm, roles: all. Required: address_id. Correct one of a business contact's addresses.
- `lg contacts update-bank-account` (API name: `update_business_contact_bank_account`) — write, gate: confirm + irreversible, roles: all. Required: bank_account_id. Correct one of a business contact's bank accounts: IBAN, account holder, bank name, BIC/SWIFT, currency, label, account type, branch code or account number.
- `lg contacts update-person` (API name: `update_business_contact_person`) — write, gate: confirm, roles: all. Required: person_id. Correct a contact person's details.

### containers
- `lg containers calculate` (API name: `decode_container_number`) — read, gate: —, roles: all. Required: container_number. Decode and validate a container number (konteyner numarası) per ISO 6346 — owner code, category, serial, check digit (is it correct, and what it should be) and, when known, the operating line with its public tracking…

### conversations
- `lg conversations add-file` (API name: `send_conversation_file`) — write, gate: confirm + irreversible, roles: all. Required: conversation_code, attachment_uuid. Post a file into an OFFER (demand-bid) chat as this user — e.g. a price sheet or a vehicle photo for the other company. conversation_code comes from list_conversations (rows whose subject is demand_bid). [takes a file: attachment_uuid]
- `lg conversations add-invitation` (API name: `invite_to_conversation`) — write, gate: confirm + irreversible, roles: all. Required: email. Invite a person from ANOTHER company into a load chat by e-mail (sohbete dışarıdan davet).
- `lg conversations add-message` (API name: `send_conversation_message`) — write, gate: confirm + irreversible, roles: all. Required: message. Post a message into a load/bid conversation AS THIS USER (not as an assistant).
- `lg conversations add-participants` (API name: `add_conversation_participants`) — write, gate: confirm, roles: all. Required: user_ids. Add people to a load/demand conversation (sohbete kişi ekle) — "Ayşe'yi FKZ8515 sohbetine ekle".
- `lg conversations create` (API name: `start_bid_conversation`) — write, gate: confirm + irreversible, roles: shipper. Required: bid_code. Open the freight chat with a carrier that bid on one of OUR demands (navlun sohbetini başlat) — bid_code comes from get_demand / the bids of a demand.
- `lg conversations list` (API name: `list_conversations`) — read, gate: —, roles: all. Required: none. List the chat conversations this user is a member of (load chats, bid chats and private carrier channels), newest activity first.
- `lg conversations list-files` (API name: `list_conversation_files`) — read, gate: —, roles: all. Required: none. List the files shared in a load/bid chat, newest first: document type, title, file name, who posted it and when.
- `lg conversations list-invitations` (API name: `list_conversation_invitations`) — read, gate: —, roles: all. Required: none. Show the e-mail invitations to a load chat that have not been accepted yet: who was invited, by whom and when, and what they will be allowed to see.
- `lg conversations list-messages` (API name: `list_conversation_messages`) — read, gate: —, roles: all. Required: none. Read the most recent messages of one conversation — give it a load code (e.g.
- `lg conversations list-participants` (API name: `list_conversation_participants`) — read, gate: —, roles: all. Required: none. Who is in this conversation — name and company of every participant.
- `lg conversations mark-read` (API name: `mark_conversation_read`) — write, gate: —, roles: all. Required: none. Mark one conversation as read for this user (clears its unread badge), or with all=true every load, bid and direct-message chat at once.
- `lg conversations remove-message` (API name: `delete_conversation_message`) — write, gate: confirm + irreversible, roles: all. Required: message_id. Delete one of THIS user's own messages from a conversation (message_id from list_conversation_messages with `mine` true).
- `lg conversations remove-participant` (API name: `remove_conversation_participant`) — write, gate: confirm + irreversible, roles: all. Required: none. Remove someone from a load/bid chat, cancel a pending e-mail invitation to it, or leave it yourself (give your own user_id).
- `lg conversations update-message` (API name: `update_conversation_message`) — write, gate: confirm, roles: all. Required: message_id, message. Edit one of THIS user's own messages in a conversation (message_id comes from list_conversation_messages, where `mine` is true).

### countries
- `lg countries search` (API name: `lookup_country`) — read, gate: —, roles: all. Required: query. 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.

### currencies
- `lg currencies list` (API name: `list_currencies`) — read, gate: —, roles: all. Required: none. The currencies the platform knows (currency_id, code, name, symbol) with the latest exchange rate against the Turkish lira (buying / selling, rate date). currency_id is what forms such as bank accounts take.

### custom-reports
- `lg custom-reports generate` (API name: `build_report`) — read, gate: —, roles: all. Required: none. Custom report counted server-side over ALL records of a dataset: filter, group (dates by month etc.), count/sum/avg/min/max (money per currency) or list rows; format=xlsx|pdf|csv gives a file.

### customs-companies
- `lg customs-companies list` (API name: `list_customs_companies`) — read, gate: —, roles: shipper. Required: none. Customs broker companies this customer can assign when accepting a bid (accept_demand_bid customs_company_id): the Logistivo customs companies you already work with in your chats.

### customs-locations
- `lg customs-locations search` (API name: `lookup_customs_location`) — read, gate: —, roles: all. Required: query. Search customs offices / customs locations (gümrük müdürlüğü, gümrük lokasyonu) by name or street — for the exit and destination customs of a load.

### demand-bid-reports
- `lg demand-bid-reports get` (API name: `get_demand_bid_report`) — read, gate: —, roles: shipper, carrier. Required: none. Bid report across ALL demands at once, counted server-side: for a customer the bids RECEIVED on its demands, for a carrier the bids it PLACED.

### demand-reports
- `lg demand-reports get` (API name: `get_demand_report`) — read, gate: —, roles: shipper, carrier. Required: none. Demand report (Talep Raporu) with optional filters: status, demanding company, transport mode, pickup / delivery countries, loading-date window.

### demand-settings
- `lg demand-settings get` (API name: `get_demand_settings`) — read, gate: —, roles: carrier. Required: none. This carrier company's demand settings: whether demands opened by forwarder companies reach the company (accept_demands_from_forwarders). [manager key]
- `lg demand-settings update` (API name: `update_demand_settings`) — write, gate: confirm, roles: carrier. Required: accept_demands_from_forwarders. Change this carrier company's demand settings. accept_demands_from_forwarders=false stops demands opened by forwarder companies from reaching the company (only shippers' demands remain); true lets them in again. [manager key]

### demand-targets
- `lg demand-targets search` (API name: `lookup_demand_target`) — read, gate: —, roles: shipper. Required: query. Resolve a pickup/delivery location for a demand to a target token ("5-<id>" = district/postal area, "10-<id>" = seaport address).

### demands
- `lg demands create` (API name: `create_demand`) — write, gate: confirm + irreversible, roles: shipper. Required: pickup_country_id, delivery_country_id, pickup_targets, delivery_targets, load_type_id, vehicle_type_ids, load_date_start, load_date_end, maturity_option_id, number_of_equipment_needed, net_kg. Open a freight demand (talep) so carriers can bid. [takes a file: attachment_uuids]
- `lg demands download` (API name: `download_demand_export`) — read, gate: —, roles: shipper. Required: demand_code, export_type. File for ONE demand owned by this company. export_type=details_pdf: the demand's detail sheet (route, dates, cargo, pallets) as PDF. export_type=sales_xlsx: the sales report workbook — demand information, every bid…
- `lg demands extract` (API name: `extract_demand_from_text`) — read, gate: —, roles: shipper. Required: text. Read a freight request text (an e-mail body, a note, a voice transcript) and return the demand fields it contains in the demand wizard's shape: transport mode, countries, ports, locations, dates, load type, vehicle…
- `lg demands get` (API name: `get_demand`) — read, gate: —, roles: shipper, carrier. Required: none. Detail of ONE freight demand.
- `lg demands list` (API name: `list_open_demands`) — read, gate: —, roles: carrier. Required: none. List freight demands (talep) this carrier is allowed to bid on, newest first.
- `lg demands list-own` (API name: `list_demands`) — read, gate: —, roles: shipper. Required: none. List the freight demands (talep) OWNED by this customer company, newest first, with route, dates, vehicles, status and bid count.
- `lg demands options` (API name: `get_demand_form_options`) — read, gate: —, roles: shipper. Required: none. 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…
- `lg demands set-status` (API name: `close_demand`) — write, gate: confirm + irreversible, roles: shipper. Required: demand_code. Close (make passive) one of this company's ACTIVE demands so carriers stop bidding.
- `lg demands update` (API name: `update_demand`) — write, gate: confirm, roles: shipper. Required: demand_code. Edit one of this company's ACTIVE demands that has NO bids yet (a demand with bids or a closed one cannot be edited). [takes a file: attachment_uuids]

### direct-conversations
- `lg direct-conversations add-message` (API name: `send_direct_message`) — write, gate: confirm + irreversible, roles: all. Required: message. Send a direct message AS THIS USER (never as an assistant) — either into an existing chat (conversation_code from list_direct_conversations) or to one person (to_user_id from list_direct_message_contacts; their private…
- `lg direct-conversations create` (API name: `create_direct_conversation`) — write, gate: confirm + irreversible, roles: all. Required: user_ids. Start a direct chat.
- `lg direct-conversations download` (API name: `export_direct_conversation_pdf`) — read, gate: —, roles: all. Required: conversation_code. Produce the whole direct-message or group chat as a PDF (the same file as the panel's export button: participants and every message with date and time) and return a short-lived download link.
- `lg direct-conversations list` (API name: `list_direct_conversations`) — read, gate: —, roles: all. Required: none. List this user's direct-message (DM) chats and group chats — person-to-person messaging, separate from load/bid chats (those are list_conversations).
- `lg direct-conversations list-contacts` (API name: `list_direct_message_contacts`) — read, gate: —, roles: all. Required: none. List the people this user can direct-message: their own teammates plus people they share a load/bid chat with.
- `lg direct-conversations list-messages` (API name: `list_direct_conversation_messages`) — read, gate: —, roles: all. Required: conversation_code. Read the latest messages of one direct-message or group chat (conversation_code from list_direct_conversations), oldest→newest, with who wrote them. `mine: true` marks this user's own messages; only those, and only…
- `lg direct-conversations mark-read` (API name: `mark_direct_conversation_read`) — write, gate: —, roles: all. Required: conversation_code. Mark one direct-message or group chat as read for this user (clears its unread badge).
- `lg direct-conversations remove-message` (API name: `delete_direct_message`) — write, gate: confirm + irreversible, roles: all. Required: conversation_code, message_id. Delete one of THIS user's own direct messages (message_id from list_direct_conversation_messages, mine and editable true), only within 5 minutes of sending.
- `lg direct-conversations update` (API name: `rename_direct_conversation`) — write, gate: confirm, roles: all. Required: conversation_code, title. Rename a GROUP direct chat (private one-to-one chats have no title).
- `lg direct-conversations update-message` (API name: `update_direct_message`) — write, gate: confirm, roles: all. Required: conversation_code, message_id, message. Edit one of THIS user's own direct messages (message_id from list_direct_conversation_messages, where mine and editable are true).

### document-checks
- `lg document-checks run` (API name: `check_document_compliance`) — write, gate: —, roles: all. Required: attachment_uuids. 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… [takes a file: attachment_uuids]

### document-types
- `lg document-types list` (API name: `list_document_types`) — read, gate: —, roles: all. Required: none. List the platform's document types (slug and name) — the values document_type_slug accepts in upload_company_document, add_driver_document and the vehicle document commands.

### documents
- `lg documents download` (API name: `get_document_link`) — read, gate: —, roles: all. Required: document_id. Get a short-lived (15 minute) download/preview link for one document by its document_id (from list_load_documents or list_company_documents).

### drive-rest-estimates
- `lg drive-rest-estimates get` (API name: `get_driver_drive_rest_estimate`) — read, gate: —, roles: carrier. Required: driver_id. One driver's GPS-ESTIMATED driving/rest status and driving time left under EU 561/2006 / AETR: current activity and since when, driving_left_today_s = the legal driving time left today under the daily, weekly (56 h) AND…
- `lg drive-rest-estimates list` (API name: `list_driver_drive_rest_estimates`) — read, gate: —, roles: carrier. Required: none. Fleet overview of the GPS-ESTIMATED driving/rest status and driving time left (EU 561/2006 / AETR): per driver the current activity, driving_left_today_s = the legal driving time left today under the daily, weekly (56…

### driver-accounts
- `lg driver-accounts get` (API name: `get_driver_account_summary`) — read, gate: —, roles: shipper, carrier. Required: none. Answer "how much do we owe this driver / how much does the driver owe us".
- `lg driver-accounts list-candidates` (API name: `list_driver_account_candidates`) — read, gate: —, roles: shipper, carrier. Required: driver_id. Before opening or filling a driver settlement statement: which of ONE driver's receipts and advances can go on it, and why the others cannot.

### driver-advances
- `lg driver-advances cancel` (API name: `cancel_driver_advance`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: advance_id, reason. Cancel (void) a recorded driver money movement that did not really happen or was entered with the wrong amount.
- `lg driver-advances create` (API name: `create_driver_advance`) — write, gate: confirm, roles: shipper, carrier. Required: driver_id, advance_kind, amount, currency_code, given_at. 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. [takes a file: attachment_uuid]
- `lg driver-advances delete` (API name: `delete_driver_advance`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: advance_id. Delete a driver money movement that was entered by mistake (duplicate, wrong driver).
- `lg driver-advances list` (API name: `list_driver_advances`) — read, gate: —, roles: shipper, carrier. Required: none. 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.
- `lg driver-advances update` (API name: `update_driver_advance`) — write, gate: confirm, roles: shipper, carrier. Required: advance_id. Correct the non-money details of a recorded driver money movement: its reference, note, or the load it belongs to.

### driver-availabilities
- `lg driver-availabilities create` (API name: `create_driver_availability`) — write, gate: confirm, roles: carrier. Required: driver_id, availability_kind, starts_on. Record that a driver is unavailable on some days — "Ali is on leave 14-20 November", "Mehmet rests at home next week", "Kemal is sick from today".
- `lg driver-availabilities delete` (API name: `delete_driver_availability`) — write, gate: confirm + irreversible, roles: carrier. Required: availability_id. Delete ONE availability period (id from list_driver_availabilities) — the driver counts as available again on those days.
- `lg driver-availabilities list` (API name: `list_driver_availabilities`) — read, gate: —, roles: carrier. Required: none. List drivers' leave, home-rest, sick and other unavailability periods (newest planning input), optionally for one driver and/or a date window.
- `lg driver-availabilities update` (API name: `update_driver_availability`) — write, gate: confirm, roles: carrier. Required: availability_id. Correct one availability period (id from list_driver_availabilities): kind, dates, Schengen presence or note.

### driver-expense-settings
- `lg driver-expense-settings get` (API name: `get_driver_expense_settings`) — read, gate: —, roles: shipper, carrier. Required: none. Read this company's driver expense module settings: is the module switched on, which currency receipts and advances are converted into, whether receipts need approval (and above which amount), whether drivers may…
- `lg driver-expense-settings update` (API name: `update_driver_expense_settings`) — write, gate: confirm, roles: shipper, carrier. Required: none. Change this company's driver expense module settings — only the fields you pass change. [manager key]

### driver-expenses
- `lg driver-expenses create` (API name: `process_receipt`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: driver_id. 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. [takes a file: attachment_uuid, attachment_uuids]
- `lg driver-expenses declare` (API name: `create_driver_expense`) — write, gate: confirm, roles: shipper, carrier. Required: driver_id, amount, currency_code. Record a driver expense WITHOUT a receipt (fişsiz şoför harcaması) — "Mehmet dün 40 EUR otopark ödedi, fişi yok", "log 120 PLN toll for driver X".
- `lg driver-expenses delete` (API name: `delete_driver_expense`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: expense_id. Delete a driver expense entered by mistake (duplicate receipt, wrong driver, test entry).
- `lg driver-expenses download` (API name: `download_driver_receipt_pack`) — read, gate: —, roles: shipper, carrier. Required: background_job_id. Get the download link of a receipt pack that generate_driver_receipt_pack prepared in the background.
- `lg driver-expenses generate` (API name: `generate_driver_receipt_pack`) — read, gate: —, roles: shipper, carrier. Required: none. Produce ONE PDF that contains the driver receipts of a selection — for a trip, a load, a driver statement or a date window — e.g. to hand to accounting or the customer.
- `lg driver-expenses get` (API name: `get_driver_expense`) — read, gate: —, roles: shipper, carrier. Required: expense_id. 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.
- `lg driver-expenses list` (API name: `list_driver_expenses`) — read, gate: —, roles: shipper, carrier. Required: none. List driver expense receipts (fuel, toll, ferry, parking, food, repair…) with a converted total.
- `lg driver-expenses list-loads` (API name: `list_driver_expense_loads`) — read, gate: —, roles: shipper, carrier. Required: driver_id. For ONE driver: which loads they carried and, per load, how many receipts there are, how many still await approval, how much is reimbursable to the driver, how much is company-billed (fuel card / DKV / UTA — never the…
- `lg driver-expenses list-trips` (API name: `list_driver_expense_trips`) — read, gate: —, roles: shipper, carrier. Required: driver_id. For ONE driver: which trips (load groups) they drove and, per trip, the number of loads and the driver's OWN receipt figures (count, awaiting approval, reimbursable, company-billed fuel-card / DKV / UTA, still…
- `lg driver-expenses review` (API name: `review_driver_expense`) — write, gate: confirm, roles: shipper, carrier. Required: status. 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…
- `lg driver-expenses set-in-account` (API name: `set_driver_expenses_in_account`) — write, gate: confirm, roles: shipper, carrier. Required: expense_ids. Count driver receipts INTO the driver's account (include=true, default) or take them OUT of it (include=false).
- `lg driver-expenses update` (API name: `update_driver_expense`) — write, gate: confirm, roles: shipper, carrier. Required: expense_id. Correct a recorded driver expense (receipt or no-receipt): amount, currency, date, category, how it was paid, merchant, description or the load it belongs to — "the parking was 45 EUR not 40", "that fuel was paid with…
- `lg driver-expenses update-conversion` (API name: `retry_driver_expense_conversion`) — write, gate: —, roles: shipper, carrier. Required: expense_id. Retry the currency conversion of ONE driver receipt whose exchange rate could not be resolved (it then sits outside every total and cannot go into a driver statement).

### driver-schedules
- `lg driver-schedules accept` (API name: `apply_driver_schedule_suggestion`) — write, gate: confirm, roles: carrier. Required: suggestion_id. Apply ONE schedule suggestion (suggestion_id from get_driver_schedule): the proposed driver is assigned to the load (or every consignment of the trip — consignments still held by another driver are taken over, and the…
- `lg driver-schedules generate` (API name: `generate_driver_schedule`) — write, gate: confirm, roles: carrier. Required: date_from, date_to. Compute schedule suggestions for a 7-90 day window starting within 30 days: which driver should take each unassigned load, trip and route plan so that nobody runs out of Schengen days, leave is respected and vehicles do…
- `lg driver-schedules get` (API name: `get_driver_schedule`) — read, gate: —, roles: carrier. Required: schedule_id. Read one schedule run (schedule_id from generate_driver_schedule / list_driver_schedules): summary (jobs covered, unassignable jobs and why, vehicle idle days before → after, lowest Schengen margin), the suggestions in…
- `lg driver-schedules list` (API name: `list_driver_schedules`) — read, gate: —, roles: carrier. Required: none. List the company's schedule-suggestion runs, newest first: id (schedule_id), status (queued | running | completed | failed), the planned window, objective and whether the run is out of date (is_stale — planning inputs…
- `lg driver-schedules reject` (API name: `dismiss_driver_schedule_suggestion`) — write, gate: —, roles: carrier. Required: suggestion_id. Dismiss ONE open schedule suggestion (suggestion_id from get_driver_schedule) so it is no longer offered; nothing is assigned or changed elsewhere.

### driver-statements
- `lg driver-statements cancel` (API name: `cancel_driver_account`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: account_id, reason. Cancel a statement that should not exist (opened by mistake, duplicated).
- `lg driver-statements create` (API name: `create_driver_account`) — write, gate: confirm, roles: shipper, carrier. Required: driver_user_id, scope_type. Open a NEW draft settlement statement for a driver.
- `lg driver-statements download` (API name: `download_driver_account_pdf`) — read, gate: —, roles: shipper, carrier. Required: account_id. Give a short-lived download link to the PDF statement of a FINALISED driver settlement statement — the document the driver sees and signs, with every line, the fuel-card block that is outside the driver maths, and the…
- `lg driver-statements finalize` (API name: `finalize_driver_account`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: account_id, expected_balance, expected_line_count, expected_currency, closing_action. 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. [manager key]
- `lg driver-statements get` (API name: `get_driver_account`) — read, gate: —, roles: shipper, carrier. Required: account_id. 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…
- `lg driver-statements list` (API name: `list_driver_accounts`) — read, gate: —, roles: shipper, carrier. Required: none. 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…
- `lg driver-statements reopen` (API name: `reopen_driver_account`) — write, gate: confirm, roles: shipper, carrier. Required: account_id, reason. Reopen a FINALISED statement so it can be corrected (a late receipt arrived, a line was wrong). [manager key]
- `lg driver-statements update` (API name: `update_driver_account`) — write, gate: confirm, roles: shipper, carrier. Required: account_id. Change the note printed on a DRAFT driver settlement statement (e.g. "September trips, fuel excluded").
- `lg driver-statements update-lines` (API name: `update_driver_account_lines`) — write, gate: confirm, roles: shipper, carrier. Required: account_id. Attach receipts/advances to a DRAFT statement, or detach ones that should not be on it.

### driver-statuses
- `lg driver-statuses list` (API name: `list_fleet_driver_statuses`) — read, gate: —, roles: shipper, carrier. Required: none. Fleet command centre — every driver's live operational status in one list: last position and its AGE, signal health (streaming / delayed / stale / no_signal / permission_denied / app_missing), duty (on_trip / assigned /…

### drivers
- `lg drivers add-document` (API name: `add_driver_document`) — write, gate: confirm, roles: carrier. Required: driver_id. 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. [takes a file: attachment_uuid]
- `lg drivers archive` (API name: `archive_driver`) — write, gate: confirm + irreversible, roles: carrier. Required: driver_id. Archive (deactivate) a driver: their account is disabled, open mobile sessions are revoked and the login link stops working.
- `lg drivers calculate` (API name: `check_driver_for_trip`) — read, gate: —, roles: carrier. Required: driver_id. Check whether ONE driver can take a job: Schengen 90/180 days left at the end of the trip (expected and worst case, counting the drive back out and the driver's later planned work), leave / home-rest / sick periods,…
- `lg drivers create` (API name: `create_driver`) — write, gate: confirm, roles: carrier. Required: first_name, last_name. Create a new driver account for this carrier company.
- `lg drivers delete-document` (API name: `delete_driver_document`) — write, gate: confirm + irreversible, roles: carrier. Required: document_id. Remove a driver paper from the driver's file.
- `lg drivers get` (API name: `get_driver`) — read, gate: —, roles: carrier. Required: driver_id. 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…
- `lg drivers list` (API name: `list_drivers`) — read, gate: —, roles: carrier. Required: none. List this carrier company's drivers with their status, current country, expiring/expired document counts, assigned load codes and vehicle plates.
- `lg drivers list-candidates` (API name: `list_trip_driver_candidates`) — read, gate: —, roles: carrier. Required: none. Rank the company's drivers for a job — which drivers are available and can do it (Schengen days at trip end incl. the drive back, leave/home-rest, other jobs, estimated driving/rest readiness, distance to the start)…
- `lg drivers list-documents` (API name: `list_driver_documents`) — read, gate: —, roles: carrier. Required: driver_id. 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.
- `lg drivers list-locations` (API name: `list_driver_locations`) — read, gate: —, roles: carrier. Required: driver_id. A driver's GPS trail — the raw position pings from the driver app in a time window, oldest to newest, with the first and last timestamps and the point count.
- `lg drivers login-link` (API name: `get_driver_login_link`) — write, gate: confirm, roles: carrier. Required: driver_id. Produce the driver's 48-hour mobile app login link (sensitive credential).
- `lg drivers request-location` (API name: `request_driver_location`) — write, gate: —, roles: carrier. Required: none. Ask a driver's phone for a fresh position ("where is Ahmet right now — ping him").
- `lg drivers search` (API name: `lookup_driver`) — read, gate: —, roles: carrier. Required: query. Resolve one of this company's drivers by name to its driver_id.
- `lg drivers update` (API name: `update_driver`) — write, gate: confirm, roles: carrier. Required: driver_id, first_name, last_name. Correct a driver's first/last name.
- `lg drivers update-document` (API name: `update_driver_document`) — write, gate: confirm, roles: carrier. Required: document_id. 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.
- `lg drivers update-login-link` (API name: `refresh_driver_login_link`) — write, gate: confirm + irreversible, roles: carrier. Required: driver_id. Generate a NEW 48-hour mobile login link for a driver and kill the old one (lost phone, expired or leaked link).

### equipment-damages
- `lg equipment-damages create` (API name: `create_equipment_damage`) — write, gate: confirm, roles: all. Required: none. Open a damage record on one of the company's own vehicles or trailers (dorse hasar kaydı aç) — "33ABC12 dorsesinin sol arka kapısında ezik var", "record a scratch on the rear bumper of the truck".
- `lg equipment-damages delete` (API name: `delete_equipment_damage`) — write, gate: confirm + irreversible, roles: all. Required: damage_id. Delete a damage record that was opened by mistake (a duplicate, the wrong vehicle).
- `lg equipment-damages generate` (API name: `export_equipment_damages`) — read, gate: —, roles: all. Required: none. Produce the equipment damage ledger as an Excel file (the same file as the panel's export button) and return a short-lived download link — e.g. "give me this year's open trailer damages as Excel".
- `lg equipment-damages list` (API name: `list_equipment_damages`) — read, gate: —, roles: all. Required: none. 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…
- `lg equipment-damages update` (API name: `update_equipment_damage`) — write, gate: confirm, roles: all. Required: damage_id. Update an existing damage record: mark it repaired (with repair date, cost and note), dismissed or disputed, change severity/type/area/description, set the estimated cost, or set/clear the responsible driver.

### equipment-inspection-links
- `lg equipment-inspection-links create` (API name: `create_equipment_inspection_link`) — write, gate: confirm, roles: all. Required: event_type. Create a one-time link so a driver or agent who is NOT on the platform (port agent, another company's driver) can upload the hand-over photos of one of the company's trailers/vehicles from a phone, without an account.

### equipment-inspections
- `lg equipment-inspections create` (API name: `create_equipment_inspection`) — write, gate: confirm, roles: all. Required: event_type. Record an equipment hand-over inspection of one of the company's own trailers or vehicles on behalf of a driver or the office — "record the pickup inspection of trailer 34ABC12 with these 4 photos, driver Ali".… [takes a file: photos[].attachment_uuid]
- `lg equipment-inspections delete` (API name: `delete_equipment_inspection`) — write, gate: confirm + irreversible, roles: all. Required: inspection_id. Delete an equipment inspection that was recorded by mistake (wrong vehicle, duplicate), together with its photos.
- `lg equipment-inspections get` (API name: `get_equipment_inspection`) — read, gate: —, roles: all. Required: inspection_id. Full detail of ONE equipment hand-over inspection: vehicle, event (pickup / dropoff / interim), who did it and where and when, the AI analysis status with its overall condition and summary, the damages that inspection…
- `lg equipment-inspections list` (API name: `list_equipment_inspections`) — read, gate: —, roles: all. Required: none. 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…
- `lg equipment-inspections run` (API name: `reanalyze_equipment_inspection`) — write, gate: confirm, roles: all. Required: inspection_id. Run the AI damage analysis again on the photos of one equipment inspection (for example after the analysis failed, or photos were added later).

### equipment-settings
- `lg equipment-settings get` (API name: `get_equipment_settings`) — read, gate: —, roles: all. Required: none. Read the company's equipment inspection settings: whether the module is on, at which hand-overs an inspection is required (pickup / dropoff), whether photos are required, the minimum photo count, which angles are…
- `lg equipment-settings update` (API name: `update_equipment_settings`) — write, gate: confirm, roles: all. Required: none. Change the company's equipment inspection settings — e.g. "turn on equipment inspection and require at least 4 photos with front, rear, left and right at pickup". [manager key]

### equipment-timelines
- `lg equipment-timelines get` (API name: `get_equipment_timeline`) — read, gate: —, roles: all. Required: none. Who has one trailer/vehicle right now (custody: in custody of a driver since the last pickup, or released at the last dropoff), the inspections it went through (newest first) and its OPEN damages — in one read.

### export-documents
- `lg export-documents add-item` (API name: `add_export_document_item`) — write, gate: —, roles: all. Required: document_id, item. Append a line item to an export document.
- `lg export-documents create` (API name: `create_export_document_draft`) — write, gate: —, roles: all. Required: doc_type. Create a new export document draft.
- `lg export-documents create-set` (API name: `create_export_document_set`) — write, gate: —, roles: all. Required: doc_types. Create SEVERAL export document drafts (a set, e.g. commercial invoice + packing list) from ONE source in one go: the source is read once by AI and every chosen document is filled from it. [takes a file: attachment_uuid]
- `lg export-documents delete` (API name: `delete_export_document`) — write, gate: confirm + irreversible, roles: all. Required: document_id. Delete one export document of this company (draft or generated).
- `lg export-documents duplicate` (API name: `duplicate_export_document`) — write, gate: —, roles: all. Required: document_id. Copy an existing export document into a NEW draft of the same type (same parties, items and terms; no document number, not generated).
- `lg export-documents extract` (API name: `extract_export_document_from_text`) — write, gate: —, roles: all. Required: document_id. Read a pasted order e-mail, offer or confirmation — or an Excel/CSV/TXT item list given as attachment_uuid — and fill an export document from it. [takes a file: attachment_uuid]
- `lg export-documents generate` (API name: `generate_export_document`) — write, gate: confirm + irreversible, roles: all. Required: document_id. 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.
- `lg export-documents generate-set` (API name: `generate_export_document_set`) — write, gate: confirm + irreversible, roles: all. Required: document_ids. Generate several export documents as PDFs in one go and return a short-lived download link for each generated one.
- `lg export-documents get` (API name: `get_export_document`) — read, gate: —, roles: all. Required: document_id. 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.
- `lg export-documents list` (API name: `list_export_documents`) — read, gate: —, roles: all. Required: none. 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).
- `lg export-documents options` (API name: `get_export_document_options`) — read, gate: —, roles: all. Required: none. List the export document types and the ready-made document sets with their credit cost.
- `lg export-documents preview` (API name: `plan_export_document_generation`) — read, gate: —, roles: all. Required: document_ids. Before generating several export documents as PDFs, show what it would cost: which documents would spend a credit, which are free (already generated), the total, the remaining balance and the required fields still empty…
- `lg export-documents remove-item` (API name: `remove_export_document_item`) — write, gate: —, roles: all. Required: document_id, position, expect_description. Delete one line item by its 1-based position.
- `lg export-documents set` (API name: `set_export_document_fields`) — write, gate: —, roles: all. Required: document_id, fields. Set one or more fields on an export document draft.
- `lg export-documents update-item` (API name: `update_export_document_item`) — write, gate: —, roles: all. Required: document_id, position, changes. Change columns of one existing line item, addressed by its 1-based position from get_export_document.

### finance
- `lg finance calculate` (API name: `generate_budget_forecast`) — read, gate: —, roles: all. Required: none. Build a budget / financial forecast (bütçe tahmini, gelecek yıl bütçesi, nakit/ciro projeksiyonu) from the company's MEASURED history, month by month.
- `lg finance get` (API name: `get_financial_snapshot`) — read, gate: —, roles: all. Required: none. 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.

### fleet-documents
- `lg fleet-documents list` (API name: `list_expiring_documents`) — read, gate: —, roles: shipper, carrier. Required: none. List fleet documents that expire soon (or already expired): vehicle papers (insurance, inspection, permits) and — for carriers — driver papers (passport, visa, licence, SRC).

### freight-invoices
- `lg freight-invoices archive` (API name: `archive_freight_invoice`) — write, gate: confirm + irreversible, roles: all. Required: freight_invoice_id. Archive (remove from the active list) a freight invoice YOUR company uploaded — e.g. a duplicate or the wrong file.
- `lg freight-invoices create` (API name: `upload_freight_invoices`) — write, gate: confirm, roles: all. Required: load_code, attachment_uuids. Attach 1-20 freight invoices (navlun faturası) to a load. [takes a file: attachment_uuids]
- `lg freight-invoices list` (API name: `list_freight_invoices`) — read, gate: —, roles: all. Required: load_code. List the freight invoices (navlun faturaları) of a load that your company is a party to: who uploaded it, amount and currency, invoice date, the AI reading status, and which load parties it is shared with.
- `lg freight-invoices set-visibility` (API name: `set_freight_invoice_visibility`) — write, gate: confirm, roles: all. Required: freight_invoice_id, visible_to_company_ids. Choose which load parties can see a freight invoice YOUR company uploaded — e.g. "share the invoice with the receiver too", "only we should see it".

### freight-reports
- `lg freight-reports get` (API name: `get_freight_cost_report`) — read, gate: —, roles: shipper, carrier. Required: none. Freight (navlun) cost report over the loads this company can see: agreed freight totals per currency plus the EUR equivalent, freight-invoice totals, how many loads have NO freight recorded (listed by code), a breakdown…

### fuel-cards
- `lg fuel-cards create` (API name: `create_fuel_card`) — write, gate: confirm, roles: shipper, carrier. Required: provider, card_number. Register a fuel card (DKV, UTA and the like) in the fleet card book, optionally tied to a driver or a vehicle.
- `lg fuel-cards delete` (API name: `delete_fuel_card`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: fuel_card_id. Delete a fuel / toll card (DKV, UTA, Shell…) that was entered BY MISTAKE.
- `lg fuel-cards list` (API name: `list_fuel_cards`) — read, gate: —, roles: shipper, carrier. Required: none. 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.
- `lg fuel-cards update` (API name: `update_fuel_card`) — write, gate: confirm, roles: shipper, carrier. Required: fuel_card_id. 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.

### handoff-links
- `lg handoff-links generate` (API name: `get_browser_handoff_link`) — read, gate: —, roles: all. Required: purpose. Get the logistivo.com link for work that deliberately stays in the browser: package_purchase (buy or upgrade a package; optional package_slug from list_packages and billing_period monthly|yearly pre-select it — the…

### import-duties
- `lg import-duties get` (API name: `get_import_duty_rates`) — read, gate: —, roles: all. Required: tariff_code. 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.

### integration-keys
- `lg integration-keys list` (API name: `list_integration_keys`) — read, gate: —, roles: all. Required: none. List the company's integration (API) keys: name, uuid, which teammate it acts as, scope (read/write) and allowed domains, access level (manager/employee), environment (live/sandbox), status (active/revoked/expired),… [manager key]
- `lg integration-keys revoke` (API name: `revoke_integration_key`) — write, gate: confirm + irreversible, roles: all. Required: key_uuid. Revoke one of the company's integration (API) keys by its uuid from list_integration_keys: the key stops working immediately and cannot be re-enabled (issue a new key instead). [manager key]

### invitations
- `lg invitations create` (API name: `invite_teammate`) — write, gate: confirm + irreversible, roles: all. Required: none. Invite colleagues to this company by e-mail — one address in email, or up to 20 at once in emails (all-or-nothing: the seat limit and every address's domain are checked before anything is sent). [manager key]
- `lg invitations list` (API name: `list_pending_invitations`) — read, gate: —, roles: all. Required: none. 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.
- `lg invitations resend` (API name: `resend_invitation`) — write, gate: confirm + irreversible, roles: all. Required: invitation_code. Re-send a pending teammate invitation e-mail and extend its expiry by 14 days. [manager key]
- `lg invitations revoke` (API name: `revoke_invitation`) — write, gate: confirm, roles: all. Required: invitation_code. Cancel a pending teammate invitation so its link stops working (it can later be re-sent). [manager key]

### invoice-settings
- `lg invoice-settings get` (API name: `get_invoice_settings`) — read, gate: —, roles: all. Required: none. 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,…

### invoice-templates
- `lg invoice-templates create` (API name: `create_invoice_template`) — write, gate: confirm + irreversible, roles: all. Required: invoice_template_id. Create a new invoice template configuration for your company: a base template plus field values (colours, font, header / footer text, IBANs shown, QR code, language; logo and signature from uploaded images). [manager key] [takes a file: logo_attachment_uuid, signature_image_attachment_uuid]
- `lg invoice-templates delete` (API name: `delete_invoice_template`) — write, gate: confirm + irreversible, roles: all. Required: template_config_id. Delete one of your invoice template configurations. [manager key]
- `lg invoice-templates get` (API name: `get_invoice_template`) — read, gate: —, roles: all. Required: none. Show the fillable fields of an invoice template (logo, colours, font, header / footer text, IBANs shown, QR code, signature, language) with their types, options and defaults — and, for one of your configurations, its…
- `lg invoice-templates set-default` (API name: `set_default_invoice_template`) — write, gate: confirm, roles: all. Required: template_config_id. Make one of your invoice template configurations the company default; the previous default stops being default. [manager key]
- `lg invoice-templates update` (API name: `update_invoice_template`) — write, gate: confirm + irreversible, roles: all. Required: template_config_id. Edit one of your invoice template configurations: switch its base template, rename it, change field values (merged over the stored ones — only the keys you pass change), replace the logo / signature image, or make it… [manager key] [takes a file: logo_attachment_uuid, signature_image_attachment_uuid]

### invoices
- `lg invoices add-line` (API name: `add_invoice_line`) — write, gate: —, roles: all. Required: invoice_id, description, quantity, unit_price. Add a line item to a DRAFT invoice and recompute totals (tax auto-resolved if tax_rate_id omitted).
- `lg invoices add-payment` (API name: `record_invoice_payment`) — write, gate: confirm + irreversible, roles: all. Required: invoice_id, amount. 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.
- `lg invoices cancel` (API name: `cancel_invoice`) — write, gate: confirm + irreversible, roles: all. Required: invoice_id. Cancel an ISSUED invoice this company issued: posts the reversing journal entry and marks the invoice cancelled.
- `lg invoices create` (API name: `create_invoice_draft`) — write, gate: —, roles: all. Required: recipient_contact_id, currency_code. Create a DRAFT invoice (reversible) issued by the current company to a recipient business contact.
- `lg invoices delete-draft` (API name: `delete_invoice_draft`) — write, gate: confirm + irreversible, roles: all. Required: invoice_id. Delete a DRAFT invoice of this company together with its lines and taxes.
- `lg invoices download` (API name: `download_invoice_pdf`) — read, gate: —, roles: all. Required: invoice_id. Get a short-lived download link for the PDF of an invoice visible to this company (and the e-invoice XML when one exists).
- `lg invoices get` (API name: `get_invoice`) — read, gate: —, roles: all. Required: none. Read one invoice in full: parties, load link, dates, every line with tax, tax breakdown, totals, remaining balance and the payments recorded against it.
- `lg invoices issue` (API name: `issue_invoice`) — write, gate: confirm + irreversible, roles: all. Required: invoice_id. Issue (finalize) a DRAFT invoice: assigns a number and posts accounting entries.
- `lg invoices list` (API name: `list_invoices`) — read, gate: —, roles: all. Required: none. List invoices visible to this company — the ones it issued and the ones issued to it (as a linked party).
- `lg invoices preview` (API name: `preview_invoice`) — read, gate: —, roles: all. Required: invoice_id. Show the user a full preview of a DRAFT invoice (recipient, line items, tax breakdown, totals) WITHOUT issuing it. [deprecated: 2026-09-03 → get_invoice]
- `lg invoices remove-line` (API name: `remove_invoice_line`) — write, gate: confirm, roles: all. Required: invoice_id, line_id. Remove one line from a DRAFT invoice your company issued; the totals are recalculated.
- `lg invoices reverse-payment` (API name: `reverse_invoice_payment`) — write, gate: confirm + irreversible, roles: all. Required: invoice_id, payment_id. 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.
- `lg invoices update` (API name: `update_invoice_draft`) — write, gate: —, roles: all. Required: invoice_id. Edit the header of a DRAFT invoice your company issued: recipient contact, currency, issue / due date, the printed note and the invoice template.
- `lg invoices update-line` (API name: `update_invoice_line`) — write, gate: —, roles: all. Required: invoice_id, line_id. Edit one line of a DRAFT invoice your company issued (text, quantity, unit, net unit price, discount, tax rate, order).

### join-requests
- `lg join-requests approve` (API name: `approve_join_request`) — write, gate: confirm + irreversible, roles: all. Required: join_request_id. Approve a pending join request: the applicant becomes a user of our company with the EMPLOYEE role, takes one team seat and is e-mailed the decision. [manager key]
- `lg join-requests list` (API name: `list_join_requests`) — read, gate: —, roles: all. Required: none. List PENDING requests from people asking to join our company (they signed up with an e-mail on one of our verified domains and picked our company). [manager key]
- `lg join-requests reject` (API name: `reject_join_request`) — write, gate: confirm + irreversible, roles: all. Required: join_request_id. Reject a pending join request: the applicant does not join our company and is e-mailed the decision. [manager key]

### journal-entries
- `lg journal-entries create` (API name: `create_journal_entry`) — write, gate: confirm + irreversible, roles: all. Required: entry_date, description, lines. Post a manual double-entry journal entry (yevmiye fişi).
- `lg journal-entries get` (API name: `get_journal_entry`) — read, gate: —, roles: all. Required: entry_id. Read one journal entry with its lines (account code/name, debit, credit, counterparty) and totals.
- `lg journal-entries list` (API name: `list_journal_entries`) — read, gate: —, roles: all. Required: none. List journal entries (yevmiye fişleri) of the company, newest first, with optional date window.

### language-settings
- `lg language-settings get` (API name: `get_language_settings`) — read, gate: —, roles: all. Required: none. Read the signed-in user's own language settings (dil ayarları).
- `lg language-settings update` (API name: `set_language`) — write, gate: —, roles: all. Required: none. Change the signed-in user's OWN language settings — "dilimi İngilizce yap", "e-postalarım Türkçe gelsin", "sohbet mesajlarını Almancaya çevir".

### load-ai-settings
- `lg load-ai-settings get` (API name: `get_load_ai_settings`) — read, gate: —, roles: all. Required: none. Read our company's load AI reminder setting (Settings → Loads): whether the assistant posts reminders into load chats about loads whose status was not updated (ai_warning_enabled) and on which days (warning_frequency:…
- `lg load-ai-settings update` (API name: `update_load_ai_settings`) — write, gate: confirm, roles: all. Required: none. Change our company's load AI reminders (Settings → Loads): ai_warning_enabled turns the reminders the assistant posts into load chats on or off; warning_frequency picks the days (daily, mon_wed_fri, weekly_wed,… [manager key]

### load-containers
- `lg load-containers create` (API name: `add_load_container`) — write, gate: confirm, roles: shipper, carrier. Required: load_code, container_number, shipping_line. Add a sea container number to a load (yüke konteyner ekle) — "add MSC container MSCU1234565 to FKZ8515".
- `lg load-containers delete` (API name: `delete_load_container`) — write, gate: confirm + irreversible, roles: all. Required: container_id. Delete a container record from a load (konteyneri sil) — only while its vessel tracking has NOT started (otherwise refused with INVALID_STATUS).
- `lg load-containers get` (API name: `get_load_container`) — read, gate: —, roles: all. Required: container_id. Show one container of a load and the last known position of the ship carrying it (konteyner konumu): ship name/IMO, latitude/longitude, speed, course, destination, ETA and the position time.
- `lg load-containers list` (API name: `list_load_containers`) — read, gate: —, roles: all. Required: load_code. List the sea containers recorded on a load (konteynerler) with their shipping line, whether vessel tracking has started and which ship carries them.
- `lg load-containers set-tracking` (API name: `start_load_container_tracking`) — write, gate: confirm, roles: all. Required: load_code. Start vessel tracking for a load from its containers (konteyner takibini başlat): the shipping line is queried for each container until the vessel carrying it is found; that ship is then tracked on the load's map and…

### load-documents
- `lg load-documents create` (API name: `attach_chat_file_to_load`) — write, gate: confirm + irreversible, roles: all. Required: load_code. 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… [takes a file: attachment_uuid, attachment_uuids]
- `lg load-documents list` (API name: `list_load_documents`) — read, gate: —, roles: all. Required: load_code. List the documents (CMR, invoice, packing list, customs papers, photos…) filed on a load — i.e. the files shared in that load's chat.

### load-groups
- `lg load-groups add-loads` (API name: `add_loads_to_group`) — write, gate: confirm, roles: shipper, carrier. Required: trip_code, load_codes. 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).
- `lg load-groups create` (API name: `create_load_group`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: none. 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.
- `lg load-groups download` (API name: `download_load_group_report`) — write, gate: —, roles: shipper, carrier. Required: trip_code, document. Download the route report of a groupage trip (sefer / parsiyel, code like LGV00253): route_pdf = detailed route report with map and itinerary, route_xlsx = the same as Excel, driver_sheet_pdf = the simplified driver…
- `lg load-groups get` (API name: `get_load_group`) — read, gate: —, roles: shipper, carrier. Required: trip_code. Detail of one groupage trip: consignments visible to you (load code, status, receiver, route, driver), planned date, optimised master route summary and stop count.
- `lg load-groups list` (API name: `list_load_groups`) — read, gate: —, roles: shipper, carrier. Required: none. 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.
- `lg load-groups optimize` (API name: `optimize_trip_route`) — write, gate: —, roles: shipper, carrier. Required: trip_code. 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…
- `lg load-groups remove-load` (API name: `remove_load_from_group`) — write, gate: confirm, roles: shipper, carrier. Required: trip_code, load_code. Take one consignment out of a groupage trip you own.

### load-heatmaps
- `lg load-heatmaps generate` (API name: `generate_load_heatmap_report`) — write, gate: confirm + irreversible, roles: all. Required: none. Produce the load heatmap report (PDF, with QR code and the list of your active loads: code, departure, arrival, date, weight), save it to the company's reports and return a download link.
- `lg load-heatmaps get` (API name: `get_load_heatmap`) — read, gate: —, roles: all. Required: none. Load heatmap data in words: the densest departure and arrival areas of your active loads, ranked by number of loads and by total kilograms.

### load-locations
- `lg load-locations create` (API name: `report_load_location`) — write, gate: confirm + irreversible, roles: carrier. Required: load_code. Report a position for a load on behalf of the carrier (konum bildir) — "FKZ8515 is at Kapıkule now", "the truck is at 41.0, 28.9".
- `lg load-locations list` (API name: `list_load_locations`) — read, gate: —, roles: all. Required: load_code. Show a load's position history (konum geçmişi) and its last known position — "where is FKZ8515 now", "where was it yesterday", "show the truck's track".
- `lg load-locations list-routes` (API name: `list_load_route_segments`) — read, gate: —, roles: all. Required: load_code. Show the road route segments drawn on a load's tracking map (rota parçaları), each with distance (metres) and duration (seconds): from the pickup address to the first reported position, between reported positions, and…

### load-notifications
- `lg load-notifications list-recipients` (API name: `list_load_notification_recipients`) — read, gate: —, roles: all. Required: load_code, notification_slug. Show which load parties can receive a load notification (departure, arrival, daily location) and how many of their people are subscribed per channel (e-mail, push, SMS).
- `lg load-notifications send` (API name: `send_load_notification`) — write, gate: confirm + irreversible, roles: all. Required: load_code, notification_slug, business_contact_ids. Send a departure / arrival / daily-location notification about a load to chosen load parties — e.g. "notify the receiver that FRT6470 arrived".
- `lg load-notifications send-email` (API name: `send_load_email_notification`) — write, gate: confirm + irreversible, roles: all. Required: load_code, email_type, person_ids. E-mail a departure / arrival / location update about a load to people you choose from YOUR OWN company directory (business contact persons) — e.g. "e-mail Mehmet at Arkas the location of FKZ8515".

### load-participants
- `lg load-participants create` (API name: `add_load_participant`) — write, gate: confirm, roles: all. Required: load_code, business_contact_id, participant_role. Add a party to a load (yüke taraf ekle) — "FKZ8515'e Arkas'ı alıcı olarak ekle" — or assign a sub-carrier. business_contact_id comes from lookup_business_contact (your own directory).
- `lg load-participants delete` (API name: `remove_load_participant`) — write, gate: confirm, roles: all. Required: load_code, participant_id. Remove a party or a sub-carrier from a load (yükten taraf çıkar) — "FKZ8515'ten alt taşıyıcı Yılmaz Nakliyat'ı kaldır". participant_id comes from list_load_participants.
- `lg load-participants list` (API name: `list_load_participants`) — read, gate: —, roles: all. Required: load_code. List a load's parties (yük tarafları): sender, receiver, carrier and sub-carriers, organizer, sender/receiver customs brokers — with participant_id (needed by every other load-participant command), position (0 = primary…
- `lg load-participants set-order` (API name: `reorder_load_participants`) — write, gate: confirm, roles: all. Required: load_code, participant_role, participant_ids. Put the parties of one role in a new order (load owner only); the first id becomes the primary. participant_ids must list ALL current rows of that role (from list_load_participants).
- `lg load-participants set-primary` (API name: `set_primary_load_participant`) — write, gate: confirm, roles: all. Required: load_code, participant_id. Make a party the primary one in its role (load owner only) — e.g. the main receiver or main customs broker. participant_id from list_load_participants.
- `lg load-participants update` (API name: `update_load_participant`) — write, gate: confirm, roles: all. Required: load_code, participant_id. Change one load party's settings (load owner only) — "Arkas FKZ8515'i görmesin": share_with_assigned_company (can the linked company see the load; cannot be turned off for a sub-carrier), conversation_user_ids (only…

### load-party-suggestions
- `lg load-party-suggestions approve` (API name: `confirm_load_party_suggestion`) — write, gate: confirm, roles: all. Required: load_code, business_contact_id, party_role. Accept a party suggestion: link the suggested business contact to your OWN load as sender or receiver (business_contact_id and party_role from list_load_party_suggestions).
- `lg load-party-suggestions list` (API name: `list_load_party_suggestions`) — read, gate: —, roles: all. Required: load_code. Show sender/receiver suggestions for one of your OWN loads: parties named in the load's documents that match a contact in your directory but are not linked to the load yet.
- `lg load-party-suggestions reject` (API name: `dismiss_load_party_suggestions`) — write, gate: —, roles: all. Required: load_code, items. Close wrong party suggestions on your OWN load so they are not suggested again — "bu önerileri bir daha gösterme".

### load-reports
- `lg load-reports get` (API name: `get_load_report`) — read, gate: —, roles: all. Required: none. Build a report of your ACTIVE loads, optionally filtered by sender, receiver, transport mode, departure country or arrival country. format=summary (default) answers in chat: the number of loads and how many are in each…
- `lg load-reports options` (API name: `get_load_report_options`) — read, gate: —, roles: all. Required: none. List the companies you can filter the load report by — the senders, receivers, carriers and other parties that appear on your active loads — with their company id and role.

### load-types
- `lg load-types search` (API name: `lookup_load_type`) — read, gate: —, roles: all. Required: none. Resolve a load/transport type (e.g.

### load-vehicles
- `lg load-vehicles delete` (API name: `clear_load_equipment`) — write, gate: confirm, roles: all. Required: load_code. Detach the fleet equipment from a load: both slots by default, or only the tractor unit / only the trailer.
- `lg load-vehicles get` (API name: `get_load_equipment`) — read, gate: —, roles: all. Required: load_code. 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.
- `lg load-vehicles set` (API name: `assign_vehicle_to_load`) — write, gate: confirm, roles: all. Required: load_code. Attach one of your own fleet vehicles to a load: the tractor unit / truck slot (plate) and/or the trailer slot (trailer_plate).

### loads
- `lg loads add-order-reference` (API name: `add_load_order_reference`) — write, gate: confirm + irreversible, roles: all. Required: load_code, order_type, order_no. Attach an order reference to a load (sipariş referansı ekle) — "FKZ8515'e satın alma siparişi PO-4471 ekle". order_type is the kind of order (e.g. purchase, sales), order_no the number.
- `lg loads analyze-documents` (API name: `reanalyze_load_documents`) — write, gate: —, roles: all. Required: load_code. Re-run the background AI consistency analysis of a load's documents (does the invoice contradict the CMR, the packing list, the declared weight…) and refresh the load's problem flag with the outcome.
- `lg loads archive` (API name: `archive_load`) — write, gate: confirm + irreversible, roles: all. Required: load_code. Archive one of the company's OWN loads (yükü arşivle) — "FKZ8515'i arşive al".
- `lg loads assign-driver` (API name: `assign_driver_to_load`) — write, gate: confirm, roles: carrier. Required: driver_id, load_code. Assign one of this company's drivers to a load so the driver sees it in the mobile app and starts reporting position.
- `lg loads audit-documents` (API name: `audit_load_documents`) — read, gate: —, roles: all. Required: none. Cross-check the documents already attached to loads and report, PER LOAD, whether their package counts and gross weights agree.
- `lg loads close-problem` (API name: `resolve_load_problem_flag`) — write, gate: confirm, roles: all. Required: load_code, action. 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…
- `lg loads create` (API name: `create_load`) — write, gate: confirm + irreversible, roles: all. Required: load_type_id, sending_country_id, receiving_country_id. Open a freight load (yük).
- `lg loads download` (API name: `download_load_cmr`) — read, gate: —, roles: all. Required: load_code. Generate a load's CMR consignment note as a PDF and return a short-lived download link — "FKZ8515'in CMR'ını ver".
- `lg loads extract` (API name: `extract_load_from_text`) — read, gate: —, roles: shipper. Required: text. Read a booking/order text (an e-mail body, a note, an ERP export line) and return the load fields it contains, in the quick-load form's shape: transport mode, countries, dates, load type, vehicle type, weight, goods…
- `lg loads get` (API name: `get_load`) — read, gate: —, roles: all. Required: none. Read one load in full by its code: route, dates, weight, current status, parties (sender/receiver/carrier), agreed freight (freight_amount + freight_currency) with its freight invoices (number, date, total), latest…
- `lg loads list` (API name: `list_loads`) — read, gate: —, roles: all. Required: none. List the freight loads (yük) this company can see, newest first.
- `lg loads list-associated` (API name: `list_associated_loads`) — read, gate: —, roles: all. Required: load_code. List the loads of the same multi-vehicle order as a given load (aynı siparişin diğer araçları) — every active load that shares its vehicle group code (LG…) and that your company can see, with status, date and carrier.
- `lg loads list-billing-targets` (API name: `list_load_billing_targets`) — read, gate: —, roles: all. Required: load_code. Show who you can invoice for a load: its commercial parties (sender, receiver, carrier) you are allowed to bill, each with recipient_contact_id to pass to create_invoice_draft and in_my_directory (true = already in your…
- `lg loads list-problems` (API name: `list_load_problem_flags`) — read, gate: —, roles: all. Required: none. List loads whose uploaded documents the AI consistency check found to CONTRADICT each other (e.g. invoice weight vs CMR weight).
- `lg loads list-status-history` (API name: `list_load_status_history`) — read, gate: —, roles: all. Required: load_code. Show a load's status history (durum geçmişi) in order — when it went pending, in transport, transit, delivered (done).
- `lg loads list-stops` (API name: `list_load_stops`) — read, gate: —, roles: all. Required: load_code. List the stops of a multi-stop load (çok duraklı yük — several pickups/drop-offs on one load) in route order, with type (pickup, dropoff, customs_exit, customs_entry), company, address, leg distance/duration and whether…
- `lg loads optimize` (API name: `optimize_load_stops`) — write, gate: —, roles: all. Required: load_code. Re-order the stops of a multi-stop load for the shortest driving route (pickups first, then drop-offs) and store the new order with leg distances — "optimise FKZ8515's stops".
- `lg loads options` (API name: `get_load_edit_form`) — read, gate: —, roles: all. Required: load_code. Read a load's editable fields, their current values and the allowed options (load types, vehicle types, goods classes, stackability, incoterms, movement types) — "FKZ8515'te neyi değiştirebilirim?".
- `lg loads preview-copy` (API name: `get_load_copy_template`) — read, gate: —, roles: all. Required: load_code. Prepare a copy draft of one of your company's OWN loads (yükü kopyala) — "FKZ8515'in aynısından bir tane daha açalım": transport mode, countries, addresses, load type, vehicle type, weight, goods class, incoterms, ports…
- `lg loads remove-driver` (API name: `unassign_driver_from_load`) — write, gate: confirm, roles: carrier. Required: load_code. Remove the driver from a load (the opposite of assign_driver_to_load): the load disappears from the driver's app and GPS tracking of the load stops.
- `lg loads search` (API name: `search_loads`) — read, gate: —, roles: all. Required: query. Full-text search across your loads (yük ara) — company names, pickup/delivery address, order references, load code, text written in the load chat and text read from documents shared in the chat: "içinde PO-4471 geçen…
- `lg loads set-status` (API name: `update_load_status`) — write, gate: confirm + irreversible, roles: carrier. Required: load_code, 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).
- `lg loads set-stop-status` (API name: `mark_load_stop_completed`) — write, gate: confirm, roles: carrier. Required: load_code, stop_id. Mark one stop of a multi-stop load as completed, or undo it (completed=false) — "FKZ8515's second drop-off is done".
- `lg loads set-tracking` (API name: `set_load_location_tracking`) — write, gate: confirm, roles: carrier. Required: load_code, enabled. Turn a load's GPS location tracking on or off (konum takibi) — "stop location tracking for FKZ8515", "resume tracking".
- `lg loads update` (API name: `update_load`) — write, gate: confirm + irreversible, roles: all. Required: load_code. Edit an existing load (yükü düzenle): loading date, weight, load type, vehicle types, goods class, stackability, pickup/delivery address, incoterms, movement type, loading/destination port.

### maturity-options
- `lg maturity-options list` (API name: `list_maturity_options`) — read, gate: —, roles: all. Required: none. The selectable payment terms (vade): maturity_option_id and the number of days (0 = cash / peşin). maturity_option_id is what update_company_profile takes.

### notification-settings
- `lg notification-settings get` (API name: `get_notification_settings`) — read, gate: —, roles: shipper. Required: none. Read our company's notification switches (Settings → Notifications): which e-mail notifications (type "notification") and push notifications (type "web-push") are on for the whole company — e.g. message, load, demand… [manager key]
- `lg notification-settings update` (API name: `update_notification_settings`) — write, gate: confirm, roles: shipper. Required: settings. Turn our company's notification switches on or off (Settings → Notifications) — e.g. stop load chat push notifications or message e-mails for the whole company. settings is a list of {slug, enabled}; slugs come from… [manager key]

### notifications
- `lg notifications list` (API name: `list_notifications`) — read, gate: —, roles: all. Required: none. 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.
- `lg notifications mark-read` (API name: `mark_notifications_read`) — write, gate: —, roles: all. Required: none. Mark ALL of the signed-in user's unread notifications as read (empty the bell).

### packages
- `lg packages get` (API name: `get_credit_balance`) — read, gate: —, roles: all. Required: none. 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…
- `lg packages list` (API name: `list_packages`) — read, gate: —, roles: all. Required: none. List the Logistivo packages this company can buy: slug, name, monthly price and yearly upfront total in the company's BILLING currency (TRY for Türkiye-billed companies, EUR otherwise — never converted with an exchange…

### pallets
- `lg pallets add-measurement` (API name: `add_pallet_measurement`) — write, gate: confirm, roles: all. Required: pallet_id. Record a NEW measurement for an existing pallet (dimensions in centimetres and/or gross weight in kilograms).
- `lg pallets add-proof` (API name: `add_pallet_proof`) — write, gate: —, roles: all. Required: pallet_id, attachment_uuid. Attach a proof photo to one of this company's pallets (e.g. the wrapped pallet, its label, damage on arrival). [takes a file: attachment_uuid]
- `lg pallets calculate` (API name: `complete_pallet_dimensions`) — read, gate: —, roles: all. Required: none. Suggest the missing base side of a pallet from a standard footprint (EUR 120×80, industrial 120×100, half 80×60; ±5 cm) when only one base side is known, and compute the volume when length, width and height are all…
- `lg pallets create` (API name: `create_pallet`) — write, gate: confirm, roles: all. Required: warehouse_id. Register a NEW pallet in a warehouse, optionally with its first measurement. warehouse_id is required — resolve the warehouse name with lookup_warehouse first.
- `lg pallets delete` (API name: `delete_pallet`) — write, gate: confirm + irreversible, roles: all. Required: pallet_id. Delete one of this company's pallet records.
- `lg pallets delete-proof` (API name: `delete_pallet_proof`) — write, gate: confirm + irreversible, roles: all. Required: pallet_id, proof_id. Remove one proof photo from one of this company's pallets.
- `lg pallets extract` (API name: `extract_pallet_from_photos`) — write, gate: —, roles: all. Required: attachment_uuids. Estimate a pallet's content label, dimensions (cm) and gross weight (kg) from photo(s) the user attached to THIS conversation. [takes a file: attachment_uuids]
- `lg pallets get` (API name: `get_pallet`) — read, gate: —, roles: all. Required: pallet_id. Read one pallet in full: its identity (code, label, SKU, barcode), status, warehouse and area, the FULL measurement history (each row with its id, source — manual, lidar, arcore or ai_photo — and confidence), its proof…
- `lg pallets list` (API name: `list_pallets`) — read, gate: —, roles: all. Required: none. 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.
- `lg pallets update` (API name: `update_pallet`) — write, gate: confirm, roles: all. Required: pallet_id. 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.

### parasut-contacts
- `lg parasut-contacts create` (API name: `create_parasut_contact`) — write, gate: confirm + irreversible, roles: all. Required: name, tax_number, parasut_account_type. Create a new customer or supplier contact IN PARAŞÜT and keep its local copy.
- `lg parasut-contacts list` (API name: `list_parasut_contacts`) — read, gate: —, roles: all. Required: none. List the customer / supplier contacts held in Paraşüt (the local copy synchronised from Paraşüt): parasut_contact_id (the Paraşüt id used by create_parasut_sales_invoice — NOT a business contact id), name, short name,…

### parasut-invoices
- `lg parasut-invoices create` (API name: `create_parasut_sales_invoice`) — write, gate: confirm + irreversible, roles: all. Required: parasut_contact_id, issue_date, scenario, items. Issue a SALES invoice in Paraşüt for one of this company's Paraşüt contacts, as an e-invoice (scenario commercial or basic — the recipient must be an e-invoice user) or an e-archive invoice (scenario e_archive); check…
- `lg parasut-invoices list` (API name: `list_parasut_invoices`) — read, gate: —, roles: all. Required: none. List invoices held in Paraşüt (local copy): sales invoices (default) or purchase bills, newest issue date first — invoice id and number, item type, description, issue / due date, net / gross / VAT totals, currency,…
- `lg parasut-invoices options` (API name: `get_parasut_invoice_options`) — read, gate: —, roles: all. Required: none. Return the options of the Paraşüt invoice form: income/expense categories (category_id), the Paraşüt bank and cash accounts, and — with tax_number — whether that VKN is a registered e-invoice user (then use scenario…

### parasut-settings
- `lg parasut-settings delete` (API name: `disconnect_parasut`) — write, gate: confirm + irreversible, roles: all. Required: none. Disconnect Paraşüt from this company: the saved credentials are removed, the accounting provider switches back to local when it was Paraşüt, and the LOCAL copies pulled from Paraşüt (contacts, invoices, bank accounts,… [manager key]
- `lg parasut-settings get` (API name: `get_parasut_settings`) — read, gate: —, roles: all. Required: none. Show the Paraşüt integration status of this company: whether it is connected, the Paraşüt firm number, the user name, the client id/secret MASKED (last 4 characters only), when it was connected, the token status and the… [manager key]
- `lg parasut-settings update` (API name: `connect_parasut`) — write, gate: confirm, roles: all. Required: parasut_company_id, username, client_id, client_secret, password. Connect this company's Paraşüt account or renew its credentials (replaces an existing connection). [manager key] [CLI only]

### payables
- `lg payables list` (API name: `list_payables`) — read, gate: —, roles: all. Required: none. 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.

### payment-cards
- `lg payment-cards delete` (API name: `delete_payment_card`) — write, gate: confirm + irreversible, roles: all. Required: card_id. Remove a saved payment card (card_id from list_payment_cards). [manager key]
- `lg payment-cards list` (API name: `list_payment_cards`) — read, gate: —, roles: all. Required: none. List the company's saved payment cards, MASKED (last four digits only), with the card holder name, expiry and which one is the default card the monthly subscription is charged to.
- `lg payment-cards set-default` (API name: `set_default_payment_card`) — write, gate: confirm + irreversible, roles: all. Required: card_id. Make one of the saved cards (card_id from list_payment_cards) the DEFAULT card: the monthly package payment is charged to it from now on, and every other card loses the default flag. [manager key]

### payment-instructions
- `lg payment-instructions cancel` (API name: `cancel_settlement_instruction`) — write, gate: confirm + irreversible, roles: all. Required: instruction_id, reason. CANCEL a pending bank payment instruction: the reservation on its invoices is released so they can be paid another way, and the instruction becomes "cancelled".
- `lg payment-instructions download` (API name: `download_settlement_instruction`) — read, gate: —, roles: all. Required: instruction_id. Get the bank payment instruction document (PDF) with a short-lived download link.
- `lg payment-instructions get` (API name: `get_settlement_instruction`) — read, gate: —, roles: all. Required: instruction_id. 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…
- `lg payment-instructions list` (API name: `list_settlement_instructions`) — read, gate: —, roles: all. Required: none. 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.
- `lg payment-instructions run` (API name: `execute_settlement_instruction`) — write, gate: confirm + irreversible, roles: all. Required: instruction_id. EXECUTE a pending bank payment instruction (banka talimatını yürüt / uygula): the reserved invoices are paid, the payments and journal entries are written with the instruction's effective date, and the instruction…

### planning-boards
- `lg planning-boards get` (API name: `get_planning_board`) — read, gate: —, roles: carrier. Required: none. The planning calendar: for every driver, the stretches of days with planned work inside the Schengen area (planned_in), days assumed inside (drive back, assumed_in), planned work outside (planned_out), leave / rest…

### plans
- `lg plans archive` (API name: `archive_plan`) — write, gate: —, roles: all. Required: plan_code. Close a plan because the user says the work is finished or abandoned.
- `lg plans create` (API name: `create_plan`) — write, gate: confirm, roles: all. Required: title, steps. Write down a multi-step job so it survives between conversations ("link these 5 loads to a trip, then invoice them").
- `lg plans get` (API name: `get_plan`) — read, gate: —, roles: all. Required: none. Read one plan: the steps that were agreed AND the commands that actually ran while it was active.
- `lg plans list` (API name: `list_plans`) — read, gate: —, roles: all. Required: none. List this company's plans, newest first.

### ports
- `lg ports search` (API name: `lookup_port`) — read, gate: —, roles: all. Required: query. Find a seaport by name or UN/LOCODE (liman ara) — "Mersin", "TRMER", "Trieste".

### preferences
- `lg preferences create` (API name: `remember_preference`) — write, gate: —, roles: all. Required: key, value. 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").
- `lg preferences delete` (API name: `forget_preference`) — write, gate: —, roles: all. Required: key. Forget one stored preference of this user.
- `lg preferences list` (API name: `list_preferences`) — read, gate: —, roles: all. Required: none. List what you have been asked to remember about THIS user, with the labels.

### product-brands
- `lg product-brands create` (API name: `create_product_brand`) — write, gate: confirm, roles: shipper. Required: name. Create a new product brand for this company (the panel's "new brand").
- `lg product-brands list` (API name: `list_product_brands`) — read, gate: —, roles: shipper. Required: none. List this company's product brands (id, name, code, active flag, number of products).

### product-categories
- `lg product-categories create` (API name: `create_product_category`) — write, gate: confirm, roles: shipper. Required: name. Create a new product category for this company (the panel's "new category").
- `lg product-categories list` (API name: `list_product_categories`) — read, gate: —, roles: shipper. Required: none. List this company's product categories (id, name, code, active flag, number of products).

### products
- `lg products add-image` (API name: `add_product_image`) — write, gate: —, roles: shipper. Required: product_id, attachment_uuid. Add a photo to a product's gallery. [takes a file: attachment_uuid]
- `lg products create` (API name: `create_product`) — write, gate: confirm, roles: shipper. Required: name. 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.
- `lg products delete` (API name: `delete_product`) — write, gate: confirm + irreversible, roles: shipper. Required: product_id. Delete a product from this company's catalogue.
- `lg products delete-image` (API name: `delete_product_image`) — write, gate: confirm + irreversible, roles: shipper. Required: product_id, image_id. Remove one photo from a product's gallery (image_id from get_product).
- `lg products extract` (API name: `extract_product_from_photos`) — write, gate: —, roles: shipper. Required: attachment_uuids. Read product details (name, description, SKU, barcode, unit, category, brand) from 1-5 photos of the product or its label — the panel's "fill from photo" button. [takes a file: attachment_uuids]
- `lg products get` (API name: `get_product`) — read, gate: —, roles: shipper. Required: product_id. Show one product of this company in full: code, SKU, barcode, unit, category and brand, low-stock threshold and reorder quantity, lot tracking, its photos (with image_id for delete_product_image /…
- `lg products list` (API name: `list_products`) — read, gate: —, roles: shipper. Required: none. List the products of this company: code, name, SKU/barcode, unit, category and brand, whether it is active, and its low-stock threshold.
- `lg products list-low-stock` (API name: `list_low_stock_products`) — read, gate: —, roles: shipper. Required: none. List the products whose quantity on hand is BELOW their low-stock threshold (strictly below — a product exactly at the threshold is not listed), with the quantity on hand and the threshold itself.
- `lg products options` (API name: `get_product_options`) — read, gate: —, roles: shipper. Required: none. List the units of measure a product can be held in (slug for create_product / update_product unit, with Turkish and English names).
- `lg products search` (API name: `lookup_product`) — read, gate: —, roles: shipper. Required: none. Resolve a product to its product_id + on-hand quantity.
- `lg products set-primary-image` (API name: `set_product_primary_image`) — write, gate: —, roles: shipper. Required: product_id, image_id. Make one of a product's photos its cover image (image_id from get_product).
- `lg products update` (API name: `update_product`) — write, gate: confirm, roles: shipper. Required: product_id. Correct an existing product: its name, unit, SKU, barcode, category, brand, minimum stock level, reorder quantity, lot tracking, note, or whether it is active.

### receivables
- `lg receivables get` (API name: `get_receivable_statement`) — read, gate: —, roles: all. Required: contact_id. 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).
- `lg receivables list` (API name: `list_receivables`) — read, gate: —, roles: all. Required: none. 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.

### report-files
- `lg report-files create` (API name: `create_report_file`) — read, gate: —, roles: all. Required: title, sheets. Turn a table you compiled from tool results into a downloadable Excel (default), PDF or CSV file — any custom table, list, comparison or export, with optional charts.

### reports
- `lg reports get` (API name: `get_accounting_report`) — read, gate: —, roles: all. Required: report. 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…

### reservations
- `lg reservations approve` (API name: `approve_reservation`) — write, gate: confirm, roles: shipper. Required: reservation_id. As the warehouse owner, approve a pending reservation received at your warehouse (the booker is notified).
- `lg reservations cancel` (API name: `cancel_reservation`) — write, gate: confirm + irreversible, roles: all. Required: reservation_id. Cancel (withdraw) a warehouse reservation you booked, or — as the warehouse owner (customer) — cancel a reservation received at your warehouse.
- `lg reservations complete` (API name: `complete_reservation`) — write, gate: confirm + irreversible, roles: shipper. Required: reservation_id. As the warehouse owner, mark an APPROVED reservation at one of your warehouses as completed (the vehicle came and the work is done).
- `lg reservations create` (API name: `create_reservation`) — write, gate: confirm, roles: all. Required: start_at, end_at, contact_phone. Book a warehouse dock time slot.
- `lg reservations get` (API name: `get_reservation`) — read, gate: —, roles: all. Required: reservation_id. Detail of one warehouse reservation you booked or (customer) one received at your warehouse: warehouse, area, time window, status, contact, notes, rejection reason.
- `lg reservations list` (API name: `list_reservations`) — read, gate: —, roles: all. Required: none. 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…
- `lg reservations list-slots` (API name: `list_reservation_slots`) — read, gate: —, roles: all. Required: none. Available dock time slots of one day with remaining capacity.
- `lg reservations reject` (API name: `reject_reservation`) — write, gate: confirm + irreversible, roles: shipper. Required: reservation_id. As the warehouse owner, reject a pending reservation received at your warehouse, with an optional reason shown to the booker.
- `lg reservations reschedule` (API name: `reschedule_reservation`) — write, gate: confirm, roles: shipper. Required: reservation_id, start_at, end_at. As the warehouse owner, move a PENDING or APPROVED reservation at one of your warehouses to a new time.

### roro-routes
- `lg roro-routes list` (API name: `list_nearby_roro_routes`) — read, gate: —, roles: carrier. Required: load_code. 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.
- `lg roro-routes select` (API name: `select_roro_route`) — write, gate: confirm, roles: carrier. Required: load_code, roro_transfer_id. Select the Ro-Ro (ferry) line a load you are carrying will use.
- `lg roro-routes set-required` (API name: `set_load_roro_required`) — write, gate: confirm, roles: carrier. Required: load_code, is_roro_required. Mark whether a load you are carrying will travel by Ro-Ro (ferry) or not.

### route-plans
- `lg route-plans assign-driver` (API name: `assign_driver_to_route_plan`) — write, gate: confirm, roles: carrier. Required: driver_id. Assign a driver (and, on a double-crew plan, a co-driver; optionally the vehicle/trailer) to a saved route plan (route_plan_id or route_plan_code).
- `lg route-plans calculate` (API name: `calculate_route_plan`) — write, gate: confirm, roles: carrier. Required: departs_at. Calculate a multi-stop truck journey WITHOUT saving it: truck driving time (car time × hgv_factor, capped speed), the EU 561/2006 / AETR breaks, daily and weekly rests, weekend/holiday driving bans and border waits, and…
- `lg route-plans create` (API name: `create_route_plan`) — write, gate: confirm, roles: carrier. Required: departs_at. Save a route plan as a DRAFT (same arguments as calculate_route_plan, plus name, vehicle_id, trailer_id, start_flex_days).
- `lg route-plans delete` (API name: `delete_route_plan`) — write, gate: confirm + irreversible, roles: carrier. Required: none. Cancel and delete a route plan (route_plan_id or route_plan_code).
- `lg route-plans get` (API name: `get_route_plan`) — read, gate: —, roles: carrier. Required: none. One saved route plan: stops with arrival/departure ranges, the summary (distance, truck driving time, arrival optimistic / expected / conservative, breaks, daily/weekly rests, reduced rests, border waits), Schengen days…
- `lg route-plans list` (API name: `list_route_plans`) — read, gate: —, roles: carrier. Required: none. List the company's saved route plans, newest first: code, name, status (draft | assigned | converted | cancelled), departure, crew, driver, vehicle, linked load/trip, stops, arrival range and Schengen day count.
- `lg route-plans preview` (API name: `compare_route_plan_scenarios`) — write, gate: confirm, roles: carrier. Required: none. Compare 1-6 variants of the same journey side by side — different drivers, departure days or a double crew: arrival range, driving and rest counts, Schengen days left at the end for every crew member, availability /…
- `lg route-plans remove-driver` (API name: `unassign_driver_from_route_plan`) — write, gate: confirm, roles: carrier. Required: none. Take the driver (and co-driver) off an assigned route plan (route_plan_id or route_plan_code); the plan goes back to draft and stops counting as planned Schengen days for them.
- `lg route-plans select` (API name: `link_route_plan`) — write, gate: confirm, roles: carrier. Required: none. Link a saved route plan (route_plan_id or route_plan_code) to the load or trip it was planned for — give load_code OR trip_code; give neither to remove the link.
- `lg route-plans update` (API name: `update_route_plan`) — write, gate: confirm, roles: carrier. Required: none. Change a saved route plan (route_plan_id or route_plan_code): any create_route_plan field — name, departure, stops, crew, options, vehicle.

### schengen-stays
- `lg schengen-stays calculate` (API name: `get_driver_schengen_projection`) — read, gate: —, roles: carrier. Required: driver_id. Project ONE driver's Schengen 90/180 days into the future: days used today, then how the remaining days move with the loads, trips and route plans already assigned to him (plus the drive back out) and his leave /…
- `lg schengen-stays create-set` (API name: `import_driver_schengen_periods`) — write, gate: confirm, roles: carrier. Required: attachment_uuid. Import drivers' past Schengen entry/exit periods (passport stamps) from an Excel/CSV file — one row per stay: driver code or name, entry date, exit date (empty = still inside). [takes a file: attachment_uuid]
- `lg schengen-stays download` (API name: `download_schengen_import_template`) — read, gate: —, roles: carrier. Required: none. Give a short-lived download link to the Schengen history import template, pre-filled with the company's active drivers (user code + name; entry/exit date cells empty).
- `lg schengen-stays get` (API name: `get_driver_schengen_detail`) — read, gate: —, roles: carrier. Required: driver_id. One driver's full Schengen 90/180 picture: days used (confirmed / declared / inferred), days left (signed; negative = overstay), latest exit date, breach date, when days come back (regain schedule), per-country day…
- `lg schengen-stays list` (API name: `list_schengen_compliance`) — read, gate: —, roles: carrier. Required: none. Fleet-wide Schengen 90/180 compliance: which drivers breached the 90-days-in-180 rule or are close to it, how many days each has left (days_remaining is SIGNED — negative means overstay), the latest exit date…
- `lg schengen-stays list-periods` (API name: `list_driver_schengen_periods`) — read, gate: —, roles: carrier. Required: driver_id. List the Schengen stay periods DECLARED for a driver (passport stamps / starting balance) with their period_id, entry and exit dates and day count.
- `lg schengen-stays remove-period` (API name: `delete_driver_schengen_period`) — write, gate: confirm + irreversible, roles: carrier. Required: driver_id, period_id. Delete ONE wrongly entered declared Schengen period of a driver (period_id from list_driver_schengen_periods).
- `lg schengen-stays set-baseline` (API name: `set_driver_schengen_baseline`) — write, gate: confirm, roles: carrier. Required: driver_id, baseline_mode. Set a driver's Schengen starting balance — what happened BEFORE GPS tracking: either the exact periods inside the zone from passport stamps (baseline_mode=periods) or just "X days used in the last 180"…
- `lg schengen-stays set-exemption` (API name: `set_driver_schengen_exemption`) — write, gate: confirm, roles: carrier. Required: driver_id, rule_applies. Exempt a driver from Schengen 90/180 tracking (EU/EFTA passport, residence permit, national long-stay visa) or switch the rule back on.

### settlements
- `lg settlements cancel` (API name: `cancel_settlement_batch`) — write, gate: confirm + irreversible, roles: all. Required: batch_id, reason. CANCEL a bulk collection / payment (settlement batch). [manager key]
- `lg settlements create` (API name: `create_settlement_batch`) — write, gate: confirm + irreversible, roles: all. Required: direction, instrument, counterparty_contact_id, currency_code, bank_account_id, lines. 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…
- `lg settlements get` (API name: `get_settlement_batch`) — read, gate: —, roles: all. Required: batch_id. Read one bulk collection / payment (settlement batch) in full: code, direction (in = collection, out = payment), instrument (transfer, bank instruction or cheque), status, counterparty, currency and total, value date,…
- `lg settlements list` (API name: `list_settlement_batches`) — read, gate: —, roles: all. Required: none. List bulk collection/payment batches (code, direction, instrument, contact, currency, total, value date, status), newest first.
- `lg settlements list-counterparties` (API name: `list_settlement_counterparties`) — read, gate: —, roles: all. Required: direction. List the business contacts that have OPEN local invoices in one money direction — in = customers owing us (sales invoices), out = suppliers we owe (purchase invoices) — each with its default open-invoice currency and…
- `lg settlements list-invoices` (API name: `list_settlement_invoices`) — read, gate: —, roles: all. Required: counterparty_contact_id, direction, currency_code. 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…
- `lg settlements options` (API name: `get_settlement_options`) — read, gate: —, roles: all. Required: none. Return the options of the bulk collection / payment form: allowed currencies, this company's OWN bank accounts (bank_account_id for create_settlement_batch / attach_check_to_invoices), bank instruction templates, batch…

### ship-trackings
- `lg ship-trackings create` (API name: `track_ship_for_load`) — write, gate: confirm, roles: shipper, carrier. Required: load_code. Attach a ship to a load and track it on the load's map (yüke gemi bağla), or swap the ship (replace_ship_id = the ship being replaced) — "FKZ8515 sails on MSC Gülsün, track it".
- `lg ship-trackings delete` (API name: `stop_ship_tracking`) — write, gate: confirm, roles: shipper, carrier. Required: load_code, ship_id. Stop tracking a ship on a load (gemi takibini durdur).
- `lg ship-trackings get` (API name: `get_load_ship_tracking`) — read, gate: —, roles: all. Required: load_code. Show the ship(s) tracked on a load and their position history since the load was created (yükün gemi takibi) — "where is FKZ8515's ship now".

### ships
- `lg ships search` (API name: `lookup_ship`) — read, gate: —, roles: all. Required: query. Search the ship registry (gemi sicili) by name, IMO or MMSI — "what is MSC Gülsün's IMO", "find the vessel 9839430".

### stock
- `lg stock adjust` (API name: `create_stock_adjustment`) — write, gate: confirm + irreversible, roles: shipper. Required: product_id, warehouse_id, mode, quantity, reason. 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.
- `lg stock list` (API name: `check_stock_level`) — read, gate: —, roles: shipper. Required: none. Read current on-hand stock levels.
- `lg stock list-movements` (API name: `list_stock_movements`) — read, gate: —, roles: shipper. Required: none. 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.
- `lg stock move` (API name: `create_stock_movement`) — write, gate: confirm + irreversible, roles: shipper. Required: type, product_id, quantity. Record a stock movement: inbound (giriş), outbound (çıkış), or transfer between locations.
- `lg stock options` (API name: `get_stock_options`) — read, gate: —, roles: shipper. Required: none. List the stock movement types (receipt, issue, transfer, count adjustment …) with their slug and Turkish/English names — the values create_stock_movement and list_stock_movements use.

### sub-companies
- `lg sub-companies create` (API name: `create_sub_company`) — write, gate: confirm, roles: shipper. Required: legal_name, tax_office, tax_number, address. Add a group / sub company (alt firma) to this customer company: legal_name (ticaret unvanı), tax_office (vergi dairesi), tax_number and the full address — all four required. [manager key]
- `lg sub-companies delete` (API name: `delete_sub_company`) — write, gate: confirm + irreversible, roles: shipper. Required: sub_company_id. Delete one of this customer company's group / sub companies (alt firma) by sub_company_id from list_sub_companies. [manager key]
- `lg sub-companies list` (API name: `list_sub_companies`) — read, gate: —, roles: shipper. Required: none. This customer company's group / sub companies (alt firmalar, grup şirketleri — Settings › Sub Companies): sub_company_id, legal name, tax office, tax number and address.

### tariff-chapters
- `lg tariff-chapters get` (API name: `get_tariff_chapter`) — read, gate: —, roles: all. Required: chapter. Answer "which chapter is this GTİP in / what does chapter 76 cover / what do the chapter notes say?".

### tariff-matrices
- `lg tariff-matrices get` (API name: `get_tariff_matrix`) — read, gate: —, roles: all. Required: matrix_run_id. FREE.
- `lg tariff-matrices list` (API name: `list_tariff_matrix_runs`) — read, gate: —, roles: all. Required: none. FREE.
- `lg tariff-matrices preview` (API name: `plan_tariff_matrix`) — read, gate: —, roles: all. Required: tariff_code, origins, destinations. FREE.
- `lg tariff-matrices run` (API name: `run_tariff_matrix`) — write, gate: confirm + irreversible, roles: all. Required: tariff_code, origins, destinations. Run an official duty + anti-dumping inquiry for ONE tariff code across MANY origin and destination countries at once, and record it.

### tariffs
- `lg tariffs coverage` (API name: `get_tariff_coverage`) — read, gate: —, roles: all. Required: destination_country. FREE.
- `lg tariffs extract` (API name: `extract_tariff_codes_from_invoice`) — write, gate: —, roles: all. Required: attachment_uuid. 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. [takes a file: attachment_uuid]
- `lg tariffs get` (API name: `get_tariff_inquiry`) — read, gate: —, roles: all. Required: inquiry_id. 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.
- `lg tariffs inquiry` (API name: `run_tariff_inquiry`) — write, gate: confirm + irreversible, roles: all. Required: tariff_code. Start an official duty / anti-dumping inquiry for one GTİP/HS code and a COUNTRY PAIR (origin → destination).
- `lg tariffs list` (API name: `list_tariff_inquiries`) — read, gate: —, roles: all. Required: none. 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.
- `lg tariffs search` (API name: `lookup_tariff_code`) — read, gate: —, roles: all. Required: query. Search the Turkish customs nomenclature (GTİP / HS) by goods description or by a partial code, and return matching codes with their official descriptions.

### teammates
- `lg teammates delete` (API name: `remove_teammate`) — write, gate: confirm + irreversible, roles: all. Required: user_id. Remove a person from the company team. [manager key]
- `lg teammates list` (API name: `list_teammates`) — read, gate: —, roles: all. Required: none. List the other users of this company (drivers excluded) with e-mail, active state and RBAC role (manager | employee | fleet-manager), plus seat_usage (team seats used / limit / remaining under the package). role narrows…
- `lg teammates set-role` (API name: `update_teammate_role`) — write, gate: confirm, roles: all. Required: user_id, role_slug. Change a team member's role (for example make them a manager, or move them back to employee). [manager key]
- `lg teammates set-status` (API name: `set_teammate_status`) — write, gate: confirm, roles: all. Required: user_id, is_active. Activate or deactivate a team member. [manager key]

### tender-bids
- `lg tender-bids create` (API name: `submit_tender_bid`) — write, gate: confirm + irreversible, roles: carrier. Required: tender_id. 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). [takes a file: attachment_uuid]
- `lg tender-bids get` (API name: `get_my_tender_bid`) — read, gate: —, roles: carrier. Required: tender_id. Read YOUR OWN bid on a tender: its status (submitted, revision requested, accepted, rejected), when it was submitted and which files it carries.
- `lg tender-bids set-status` (API name: `decide_tender_bid`) — write, gate: confirm + irreversible, roles: shipper. Required: tender_id, bid_id, decision. As the tender owner, ACCEPT a carrier's bid, REJECT it, or ask for a REVISION.

### tenders
- `lg tenders add-document` (API name: `add_tender_document`) — write, gate: confirm, roles: shipper. Required: tender_id, attachment_uuid. Attach a file to one of YOUR tenders as an additional specification document (accepted: pdf, xlsx, xls, doc, docx; at most 10 MB). [takes a file: attachment_uuid]
- `lg tenders create` (API name: `create_tender`) — write, gate: confirm, roles: shipper. Required: title, description, start_date, end_date, validity_start_date, validity_end_date. 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). [takes a file: attachment_uuid]
- `lg tenders delete-document` (API name: `delete_tender_document`) — write, gate: confirm + irreversible, roles: shipper. Required: tender_id, tender_document_id. Remove a specification file from one of YOUR tenders (document_id from get_tender → documents[]).
- `lg tenders extract` (API name: `extract_tender_specification`) — read, gate: —, roles: shipper. Required: attachment_uuid. Draft the specification (terms) text of a tender from a tender document the user supplies (accepted: pdf, xlsx, xls; at most 10 MB). [takes a file: attachment_uuid]
- `lg tenders generate` (API name: `generate_tender_excel`) — write, gate: confirm, roles: shipper. Required: tender_id, lanes. 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.
- `lg tenders get` (API name: `get_tender`) — read, gate: —, roles: shipper, carrier. Required: tender_id. 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.
- `lg tenders list` (API name: `list_tenders`) — read, gate: —, roles: shipper, carrier. Required: none. List document-based freight tenders.
- `lg tenders update` (API name: `update_tender`) — write, gate: confirm, roles: shipper. Required: tender_id. Edit one of YOUR tenders: title, description, specification text, the bidding window (start_date / end_date), the price-validity window, whether it still accepts bids (is_active), its status, and who it is shared with.

### trade-measures
- `lg trade-measures get` (API name: `get_trade_measures`) — read, gate: —, roles: all. Required: tariff_code. 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.
- `lg trade-measures list` (API name: `list_trade_measures`) — read, gate: —, roles: all. Required: none. 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…

### user-accounts
- `lg user-accounts preview` (API name: `preview_account_closure`) — read, gate: —, roles: all. Required: none. Show what closing MY OWN account would do, without closing it: whether I am a manager, whether I am the only user (then the company is deactivated too), whether I must first hand the manager role to a teammate…

### users
- `lg users get` (API name: `get_current_user`) — read, gate: —, roles: all. Required: none. Who am I?

### vehicle-assignments
- `lg vehicle-assignments create` (API name: `assign_vehicle_to_driver`) — write, gate: confirm, roles: shipper, carrier. Required: driver_id. 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.
- `lg vehicle-assignments list` (API name: `list_vehicle_assignments`) — read, gate: —, roles: shipper, carrier. Required: none. List which driver currently holds which vehicle (tractor unit / trailer).
- `lg vehicle-assignments revoke` (API name: `end_vehicle_assignment`) — write, gate: confirm, roles: shipper, carrier. Required: assignment_id. Close an open vehicle assignment: the driver hands the vehicle back.

### vehicle-expenses
- `lg vehicle-expenses create` (API name: `create_vehicle_expense`) — write, gate: confirm, roles: shipper, carrier. Required: none. 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. [takes a file: attachment_uuid]
- `lg vehicle-expenses delete` (API name: `delete_vehicle_expense`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: expense_id. Delete a vehicle cost entered by mistake (duplicate, wrong plate, test entry).
- `lg vehicle-expenses list` (API name: `list_vehicle_expenses`) — read, gate: —, roles: shipper, carrier. Required: none. 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.
- `lg vehicle-expenses update` (API name: `update_vehicle_expense`) — write, gate: confirm, roles: shipper, carrier. Required: expense_id. Correct a recorded vehicle cost: category, amount, currency, vendor, invoice number, invoice date or description — "the tyre invoice was 580 not 850".

### vehicles
- `lg vehicles add-document` (API name: `add_vehicle_document`) — write, gate: confirm, roles: shipper, carrier. Required: attachment_uuid. File a paper of one of the company's own vehicles — registration (ruhsat), traffic or kasko insurance, roadworthiness inspection, tachograph calibration, CMR insurance, green card, ADR certificate — from an uploaded… [takes a file: attachment_uuid]
- `lg vehicles archive` (API name: `archive_vehicle`) — write, gate: confirm, roles: shipper, carrier. Required: vehicle_id. Take a vehicle out of service (archive it) or put an archived one back into service.
- `lg vehicles create` (API name: `create_vehicle`) — write, gate: confirm, roles: shipper, carrier. Required: none. 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. [takes a file: attachment_uuid]
- `lg vehicles delete` (API name: `delete_vehicle`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: none. Delete a vehicle record that was created BY MISTAKE (wrong plate, duplicate).
- `lg vehicles delete-document` (API name: `delete_vehicle_document`) — write, gate: confirm + irreversible, roles: shipper, carrier. Required: document_id. Remove a paper from a vehicle's file (a duplicate row, a paper filed on the wrong vehicle, an old registration).
- `lg vehicles get` (API name: `get_vehicle`) — read, gate: —, roles: shipper, carrier. Required: none. 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…
- `lg vehicles list` (API name: `list_vehicles`) — read, gate: —, roles: shipper, carrier. Required: none. 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…
- `lg vehicles search` (API name: `lookup_vehicle`) — read, gate: —, roles: shipper, carrier. Required: query. 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.
- `lg vehicles update` (API name: `update_vehicle`) — write, gate: confirm, roles: shipper, carrier. Required: vehicle_id. Correct a fleet vehicle's fields: plate, kind, brand, model, model year, VIN or note.
- `lg vehicles update-document` (API name: `update_vehicle_document`) — write, gate: confirm, roles: shipper, carrier. Required: document_id. Correct a vehicle paper already on file — most often its expiry date (valid_until) after a renewal or a wrong AI reading, but also its type, number, issue date, issuer, the plate printed on it, issuing country, title or…

### wallets
- `lg wallets get` (API name: `get_wallet_balance`) — read, gate: —, roles: all. Required: none. Read this company's Logistivo WALLET balance — the prepaid MONEY balance (amount + currency).
- `lg wallets list-transactions` (API name: `list_wallet_transactions`) — read, gate: —, roles: all. Required: none. List this company's Logistivo WALLET movements, newest first: top-ups and money spent on packages or paid features, with the amount, the balance before/after, who did it and when.

### warehouse-areas
- `lg warehouse-areas create` (API name: `create_warehouse_area`) — write, gate: confirm, roles: shipper. Required: warehouse_id, name. Add a new area to one of this company's warehouses — an open area, a closed area or a loading ramp — with how many vehicles it can take at the same time (its booking slot capacity).
- `lg warehouse-areas delete` (API name: `delete_warehouse_area`) — write, gate: confirm + irreversible, roles: shipper. Required: area_id. Delete one area/ramp of a warehouse.
- `lg warehouse-areas update` (API name: `update_warehouse_area`) — write, gate: confirm, roles: shipper. Required: area_id. Change a warehouse area's name, type (open_area, closed_area, ramp), vehicle capacity, display order or active flag.

### warehouse-links
- `lg warehouse-links create` (API name: `create_warehouse_link`) — write, gate: confirm, roles: shipper. Required: warehouse_id. Create a public booking link for one of this company's warehouses, to share with a carrier, supplier or customer: anyone holding the URL can book a slot (the booking still waits for your approval).
- `lg warehouse-links list` (API name: `list_warehouse_links`) — read, gate: —, roles: shipper. Required: warehouse_id. List the public booking links shared for one of this company's warehouses: the full URL (anyone holding it can book a slot), the area it is limited to (if any), the expiry, and whether it is active.
- `lg warehouse-links revoke` (API name: `revoke_warehouse_link`) — write, gate: confirm + irreversible, roles: shipper. Required: link_id. Permanently revoke a warehouse booking link: the URL stops working for everyone and cannot be brought back (create a new link if needed).
- `lg warehouse-links set-status` (API name: `set_warehouse_link_status`) — write, gate: confirm, roles: shipper. Required: link_id, is_active. Turn a warehouse booking link off (nobody can book through it) or back on.

### warehouses
- `lg warehouses create` (API name: `create_warehouse`) — write, gate: confirm, roles: shipper. Required: name, address_line. 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.
- `lg warehouses delete` (API name: `delete_warehouse`) — write, gate: confirm + irreversible, roles: shipper. Required: warehouse_id. Delete one of this company's warehouses.
- `lg warehouses get` (API name: `get_warehouse`) — read, gate: —, roles: shipper. Required: warehouse_id. 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…
- `lg warehouses list` (API name: `list_warehouses`) — read, gate: —, roles: shipper. Required: none. 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.
- `lg warehouses search` (API name: `lookup_warehouse`) — read, gate: —, roles: shipper. Required: none. Resolve a warehouse by name to its warehouse_id, with its areas.
- `lg warehouses set-equipment` (API name: `set_warehouse_equipment`) — write, gate: confirm, roles: shipper. Required: warehouse_id, equipment. Set a warehouse's equipment list IN FULL (forklift, transpallet = pallet truck, other; with quantity and an optional name/specification).
- `lg warehouses update` (API name: `update_warehouse`) — write, gate: confirm, roles: shipper. Required: warehouse_id. 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.

## ERP and CRM integration

Once the catalog opens, third-party software joins the lg client: a customer's ERP runs a nightly sync, their CRM opens a load here when a quote is accepted. That consumer is not a person. The decisions below exist for exactly that.

### 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.

### 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.

### 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.

### 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).

### 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".

### 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.

## 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 448 commands, a carrier 452, a customs broker 312, an affiliate 55. If a command is missing from your catalog, check the role first; if it is listed but refused, check the key's scope and access level. Driver accounts have no access to this surface at all.
- Some commands spend credits: Tariff inquiries, document generation and a few other AI-backed commands draw on your plan's credit balance. Every catalog row carries credit_cost {mode, credits}: fixed is the exact charge per call, read_only never charges, and computed means the charge depends on the call — credits is then null and the real figure appears on the confirmation card. A preflight (dry_run) tells you what one call would charge and whether your balance covers it.
- Integration keys cannot drive the dashboard chat: The AI assistant inside the dashboard is for people. Starting or continuing an assistant run with an integration key is refused with 403; agents use the command endpoints, lg or the MCP channel, where every command runs at the key's access level and its writes are recorded as integration writes.
- 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.

## Related resources

- [System status & incident history](https://logistivo.com/en/status) — Live component measurement, a 90-day summary and an automatically kept incident record.
- [Connect to an AI assistant (MCP)](https://logistivo.com/en/mcp) — Signup and freight-request flow from ChatGPT, Claude or Gemini — a separate channel.
- [AI-powered logistics](https://logistivo.com/en/ai-logistics) — The capabilities behind the commands: document reading, tariff lookup, consistency checks.
- [Load management & live tracking](https://logistivo.com/en/load-management) — The record that the commands in the loads domain actually change.
- [Accounting & e-invoicing](https://logistivo.com/en/accounting-invoicing) — The double-entry accounting and e-invoicing module the invoices domain writes into.
- [Customs intelligence: HS tariff & anti-dumping](https://logistivo.com/en/customs-intelligence) — The data source and scope behind the commands in the tariffs domain: the nomenclature, measures in force and import duty rates are read from our own copy at no credit cost; a credit is spent only by an inquiry that reaches a foreign authority and returns a verdict.
- [Logistics software buyer's guide](https://logistivo.com/en/logistics-software-guide) — How to evaluate a platform's integration surface before you buy it.

## Frequently asked questions

### What is the Logistivo CLI used for?

It runs the work in your Logistivo account from a terminal, a script or your own AI agent: loads and trips, demands, bids and tenders, fleet, drivers and their expenses, documents, contacts and current accounts, invoices and accounting, warehouse and stock, team members, HS tariffs and anti-dumping duties. Every write action in the panel has a command; payments, legal consent and account closure deliberately stay in the browser. The same commands are callable over HTTP for ERP/CRM integrations and through the MCP channel.

### How are commands named in the Logistivo CLI?

The shape is lg <domain> <verb> --parameter=value. The domain is a plural entity (loads, demands, invoices, export-documents) and the verb comes from a closed vocabulary that grows only by deliberate decision (list, get, create, update, search, set-status, 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?

Interactively, lg login shows an approval code that you confirm in your browser after signing in to Logistivo — no password or key is typed into the terminal, and the session stays signed in and renews itself until you run lg logout. lg login --password (email and password) remains as a fallback. On servers and in CI, use an integration key issued in the panel or with lg keys create: lg login --api-key saves it to a local profile, or put it in the LOGISTIVO_TOKEN environment variable. 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.<entity>, 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, or from a terminal with lg keys create. The key acts on behalf of a real team member and has three independent settings: scope (read opens the catalog and non-writing commands, write opens every command — the confirmation gate stays as it is), optional domain grants, and an access level — employee, the default, for day-to-day operations, or manager for team, settings, keys and money. It is shown once and not stored on the server; it can be rotated, transferred 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=<unix>,v1=<hex>. The signed string is "<t>.<RAW BODY>", 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.

### Can my AI agent do everything a user can do in the panel?

Almost everything. Every write action in the panel has a command — measured against the panel's write endpoints, 100% of the in-scope ones are covered — and the same commands are open to lg, to HTTP and to the MCP channel. The deliberate exceptions are identity and money steps: signing in, buying a package, adding a card with 3D Secure, approving legal texts and closing an account stay in the person's browser, and lg handoff-links generate returns the link for them. Integration keys, webhooks and the sandbox are managed with lg built-in commands because they return secrets once.

### Can I pass files such as PDFs or photos to commands from the CLI or the API?

Yes. With lg, put @ in front of a local path (--attachment_uuid=@./cmr.pdf) and lg uploads the file first; --file <path> is a shortcut when a command has a single file field, and lg upload only uploads. Over HTTP, send the file as multipart to POST /api/common/commands:upload and pass the returned attachment_uuid to the command; MCP clients use the upload_attachment tool. The type is detected from the content, uploads belong to the user and stay usable for 24 hours, and uploading the same file again returns the same id.

### What is the difference between an employee and a manager integration key?

The access level decides how heavy the work a key may do is. An employee key, the default for new keys, runs day-to-day operations: loads, demands, bids, trips, fleet, documents and contacts. A manager key may also do management work — team members and roles, company settings, integration keys, bank accounts, saved cards, affiliate payouts. A key never has more rights than the person it acts for, and an employee key calling a manager command gets INSUFFICIENT_KEY_LEVEL. Change the level in the panel or with lg keys update --access-level=manager.

### Can an affiliate (referral partner) use the CLI?

Yes. An affiliate account gets a catalog of its own — 55 commands for referrals, leads, CRM notes and tasks, sales decks, earnings and payouts — and uses it from lg, HTTP or MCP like any other role. Requesting or cancelling a payout and changing the payout bank account need a manager-level key and a confirmation. A company that is also a shipper, carrier or customs broker sees both catalogs.

