> ## Documentation Index
> Fetch the complete documentation index at: https://docs.listo.delivery/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Notes on the Listo Partner Delivery API:
> - Every endpoint is under `https://api.listo.delivery/v1/` and authenticates with a bearer API key.
> - This documentation site is at https://docs.listo.delivery. The full machine-readable reference is the OpenAPI document at https://api.listo.delivery/docs/openapi.yaml (served by the API host, not by this site).
> - Any page of this site is also available as raw markdown by appending `.md` to its URL, for example https://docs.listo.delivery/en/guides/webhooks.md. The index of every page is https://docs.listo.delivery/llms.txt and all of them in one file is https://docs.listo.delivery/llms-full.txt.
> - Every page is published in English under https://docs.listo.delivery/en/ and in Spanish under https://docs.listo.delivery/es/, with the same path after the language. The API reference is under https://docs.listo.delivery/api-reference/ in English and https://docs.listo.delivery/es/api-reference/ in Spanish.
> - To debug an integration yourself: `GET /v1/request_logs` lists the calls your keys made with what you sent and what was answered, `GET /v1/events` lists the events Listo emitted, `GET /v1/webhook_deliveries` lists every attempt to deliver them, `POST /v1/events/{id}/resend` sends one event again and `POST /v1/test_webhooks` sends a sample event (`"test": true`). The page https://docs.listo.delivery/en/guides/debugging describes them. A resent event is a snapshot from when it happened; `GET /v1/deliveries` is still how you recover current state.
> - Every error response has `error.doc_url`, a page that explains that error code and what to do, and `error.request_id`, the same value as the `X-Request-Id` response header. All the pages are listed at https://docs.listo.delivery/en/errors. Filter `GET /v1/request_logs` with `request_id` to read the logged call.
> - A `409 account_exists` from `POST /v1/stores` means the restaurant already has its own Listo account: connect it instead of creating it, as the page https://docs.listo.delivery/en/guides/connect-existing-account describes.
> - On Listo's self-serve pricing the stores your account created itself carry no Plan Listo: `plan` is `null`, there is no Plan Listo per store, and the account pays for each trip and for the add-ons it switches on (monthly ones each month, per-use ones each time they are used). A restaurant that connects to a self-serve account keeps its own plan and pays it itself. On any other account every live store carries one Plan Listo, charged to the partner, unless the agreement has each restaurant pay its own. The exception is a connected restaurant that had already paid its current month: it keeps paying its own plan until its next renewal, shown as `paid_by: "store"` with the handover date in `pending_paid_by_on`. A `402 plan_required` from a quote, a delivery create or a retry means the store's plan is not paid or has ended (a store created by a self-serve account is never refused for lack of a plan, but it can still be refused with `402 plan_required` when Listo has switched its couriers off, and then the message says to contact Listo and nothing you pay changes it); the page https://docs.listo.delivery/en/guides/plan-and-billing describes the plan, the `plan` block on a store and how to end or restart it.
> - An AI assistant (Claude Code, Cursor, Codex, ChatGPT) can act on a developer account at `https://api.listo.delivery/mcp`, an MCP server over Streamable HTTP. A person connects by signing in (OAuth: the client opens a browser, the person signs in, chooses the account and allows it, optionally read-only); a script sends `Authorization: Bearer <API key>` instead. A sign-in access token works on `/mcp` only, never on `/v1/`. Every tool runs the matching `/v1/` endpoint, and each one that moves money first previews and then needs a confirmation token; the page https://docs.listo.delivery/en/guides/connect-ai-client lists the tools and describes the two steps.

# The listo command line

> Log in from your terminal, receive your webhooks on your own computer, send test events, follow your API calls and create deliveries, in test mode by default.

`listo` is a command-line tool for the Listo Delivery API. It does five things
you would otherwise build yourself:

* **Log in** without copying a key anywhere.
* **Receive your webhooks on your own computer**, signed the way production
  signs them, with no public address and no tunnel.
* **Send a test event** of any type.
* **Follow your API calls** as they happen.
* **Create, read and cancel deliveries**, and play a courier scenario on a test
  one.

Everything runs in **test mode** unless you add `--live`. Test mode uses your
test credential and moves no money (see [test mode](https://docs.listo.delivery/en/concepts/test-mode.md)).
Live mode needs `--live`, and every command that writes in live mode shows you
exactly what it is about to do and asks first.

## Install

Pick one. All three install the same program.

```bash npm theme={null}
npm install -g listo-cli
```

```bash Homebrew theme={null}
brew install listo-delivery/tap/listo
```

```bash Install script theme={null}
curl -fsSL https://cli.listo.delivery/install.sh | sh
```

The install script works on macOS and Linux (Intel and ARM). It puts `listo` in
`$HOME/.listo/bin` (set `LISTO_INSTALL_DIR` to choose another folder), prints the
line to add to your `PATH` if that folder is not on it, and installs nothing
unless two checks pass first: the signature on the release's checksum file, made
with Listo's release key, and the checksum of the download. On Windows, use npm.

Check that it works:

```bash theme={null}
listo version
```

```
listo 0.1.0 (darwin/arm64)
```

Every command has `--help` with an example. To complete commands and flags in
your shell, load the script `listo completion` prints for bash, zsh, fish or
PowerShell once from your shell profile:

```bash theme={null}
listo completion zsh > "${fpath[1]}/_listo"
```

## Log in

```bash theme={null}
listo login
```

```
Your code is ABCD-EFGH
Press Enter to open https://partners.listo.delivery/cli/confirm?code=ABCD-EFGH in your browser (or open it yourself). Compare the code on the page with the one above before you authorize.
Waiting for approval...
Done. This computer is "MacBook de Ana" in Test mode. Run listo login --live to add Live.
Saved in your system credential store.
```

**1. Compare the code**

Press Enter to open the page, or open the address yourself. The page shows a
code. **Check that it is the same one your terminal printed** before you
authorize: the code is how you know the approval is for the terminal in front
of you and not for somebody else's.

**2. Choose the mode and authorize**

The page offers **Test** (no real money) and **Live** (real money). Live is
available once your live balance has money or you have saved a card, and
approving it asks for your password. Press **Deny** if the request is not yours.

**3. Go back to the terminal**

The terminal finishes by itself. The computer now appears in the partner
dashboard, under **API keys** and then **Connected apps**, as **Listo CLI on
MacBook de Ana**, where **Disconnect** ends its access at once.

**One approval is one mode.** `listo login` signs this computer in to Test.
`listo login --live` asks for Live in the same way and adds it; the two logins
are kept apart, and `--live` on any later command selects the Live one. If the
page approves a different mode than the one you asked for, the terminal tells you
so.

Where the login is kept: in your operating system's credential store (the macOS
Keychain, the Windows Credential Manager or the Secret Service on Linux). When no
store answers, as in a container, it falls back to a file only you can read, and
the last line of the output says which one it used. `LISTO_NO_KEYRING=1` forces
the file.

`listo whoami` asks the API which company and which mode the credential is for,
and `listo logout` ends the login of one mode (`listo logout --live` for Live):
the session is revoked at Listo and the login is removed from this computer.

### With an API key instead

Already have a key from the dashboard? Save it, or just set it for one shell:

```bash theme={null}
listo login --key lst_test_...
```

```bash theme={null}
export LISTO_API_KEY=lst_test_...
listo whoami
```

`LISTO_API_KEY` wins over a saved login. A test key (`lst_test_...`) works
without `--live`, and a live key (`lst_live_...`) only with it; mixing them is
refused before any call, with the flag to add or drop.

### Without a terminal

In a script, a container or a CI job there is nobody to press Enter. Ask for the
code and finish the login from another shell:

```bash theme={null}
listo login --non-interactive
```

```json theme={null}
{
  "verification_url": "https://partners.listo.delivery/cli/confirm?code=ABCD-EFGH",
  "user_code": "ABCD-EFGH",
  "expires_in": 600,
  "next_step": "listo login --complete lso_dc_..."
}
```

Open `verification_url` yourself, compare the code and authorize. Then run the
command in `next_step` on the machine that asked, and it saves the login:

```bash theme={null}
listo login --complete lso_dc_...
```

`login` run with no terminal does the same as `--non-interactive`. The code
expires, and the terminal says so ("The code expired before it was approved")
and names the command to run again. `--key`, `--non-interactive` and `--complete`
cannot be combined.

## Receive your webhooks on your computer

```bash theme={null}
listo listen --forward-to http://localhost:3000/webhooks/listo
```

```
Ready [TEST]. Your webhook signing secret is whsec_... (set LISTO_WEBHOOK_SECRET in your app)
12:04:11  delivery.status_changed  evt_3kM7qR2vX9pNdY4tB6nZ1sLw  -> 200 in 41 ms
12:04:13  delivery.courier_location  evt_9aB4cD6eF8gH1jK3mN5pQ7rS  -> 200 in 12 ms
```

`listen` opens a live connection to your account and sends every event Listo
emits for it, in the selected mode, to the address you gave. One line per event:
the time, the type, the event id, what your server answered and how long it took.
Press Ctrl+C to stop. Nothing needs to be reachable from the internet.

### Verify exactly as you do in production

Each event is sent as a `POST` of the event's JSON body, byte for byte as Listo's
webhook sender writes it, with an `X-Listo-Signature` header (`t=...,v1=...`)
and a `User-Agent` of `listo-cli/<version>`. So the code that verifies it is the
code you ship: the SDK's verification helper, over the raw body, with a secret.

```typescript verify_webhook_signature.ts theme={null}
// Verifies the signature on an incoming Listo webhook before trusting a byte of
// it. Reads the raw request body on stdin, the way your web framework hands it
// to you, and refuses anything that does not check out.
//
//   npm i listo-delivery
//   LISTO_SIGNATURE='t=...,v1=...' LISTO_WEBHOOK_SECRETS=whsec_a,whsec_b \
//     node verify_webhook_signature.ts < body.json
//
import { readFileSync } from "node:fs";
import { Listo, ListoError } from "listo-delivery";

// The RAW bytes, before any JSON parsing. Parsing and re-serializing changes
// them (key order, escapes, whitespace) and the signature covers what was sent,
// not what your JSON library would have sent.
const payload = readFileSync(0);

try {
  // Checks the signature and the 300 second window in BOTH directions (the
  // replay this scheme exists to stop), then parses the body. `secrets` is a
  // LIST because a rotation has two secrets live at once: accept either while
  // you swap over, then drop the old one. No API key is needed.
  const event = Listo.webhooks.constructEvent(
    payload,
    process.env.LISTO_SIGNATURE,
    process.env.LISTO_WEBHOOK_SECRETS!.split(","),
  );

  // Events arrive at least once and in no guaranteed order, so treat `id` as the
  // thing you deduplicate on and the object's own timestamps as the thing you
  // order by. Answer 2xx quickly and do the work afterwards.
  console.log(`${event.type} ${event.id}`);

  switch (event.type) {
    case "delivery.status_changed":
    case "delivery.updated":
      console.log(`order ${event.data.external_id} is ${event.data.status}`);
      break;
    case "delivery.courier_location":
      console.log(`courier at ${event.data.location.lat},${event.data.location.lng}`);
      break;
    default:
      // A new type is an additive change, so ignore what you do not recognise
      // rather than failing on it.
      console.log("ignored");
  }
} catch (error) {
  // `invalid_signature` (a ListoSignatureError) and `invalid_payload` (a signed
  // body that is not a JSON object) are both a ListoError.
  if (!(error instanceof ListoError)) throw error;

  // Answer 400 and stop. Do not parse it, do not act on it, do not log it as an
  // event you received.
  console.error("signature rejected");
  process.exit(1);
}
```

```python verify_webhook_signature.py theme={null}
# Verifies the signature on an incoming Listo webhook before trusting a byte of
# it. Reads the raw request body on stdin, the way your web framework hands it
# to you, and refuses anything that does not check out.
#
#   pip install listo-delivery
#   LISTO_SIGNATURE='t=...,v1=...' LISTO_WEBHOOK_SECRETS=whsec_a,whsec_b \
#     python verify_webhook_signature.py < body.json
#
import os
import sys

from listo_delivery import ListoError, webhooks
from listo_delivery.models import (
    DeliveryCourierLocationEvent,
    DeliveryStatusChangedEvent,
    DeliveryUpdatedEvent,
)

# The RAW bytes, before any JSON parsing. Parsing and re-serializing changes
# them (key order, escapes, whitespace) and the signature covers what was sent,
# not what your JSON library would have sent.
payload = sys.stdin.buffer.read()

try:
    # Checks the signature and the 300 second window in BOTH directions (the
    # replay this scheme exists to stop), then parses the body. `secrets` is a
    # LIST because a rotation has two secrets live at once: accept either while
    # you swap over, then drop the old one. No API key is needed.
    event = webhooks.construct_event(
        payload,
        os.environ.get("LISTO_SIGNATURE"),
        os.environ["LISTO_WEBHOOK_SECRETS"].split(","),
    )
except ListoError:
    # `invalid_signature` (a ListoSignatureError) and `invalid_payload` (a signed
    # body the SDK cannot read) are both a ListoError. Answer 400 and stop. Do
    # not parse it, do not act on it, do not log it as an event you received.
    sys.exit("signature rejected")

# Events arrive at least once and in no guaranteed order, so treat `id` as the
# thing you deduplicate on and the object's own timestamps as the thing you
# order by. Answer 2xx quickly and do the work afterwards.
kind = event.type_
print(f"{kind} {event.id}")

match event:
    case DeliveryStatusChangedEvent() | DeliveryUpdatedEvent():
        print(f"order {event.data.external_id} is {event.data.status}")
    case DeliveryCourierLocationEvent():
        print(f"courier at {event.data.location.lat},{event.data.location.lng}")
    case _:
        # A new type is an additive change, so ignore what you do not recognise
        # rather than failing on it.
        print("ignored")
```

```ruby verify_webhook_signature.rb theme={null}
# Verifies the signature on an incoming Listo webhook before trusting a byte of
# it. Reads the raw request body on stdin, the way your web framework hands it
# to you, and refuses anything that does not check out.
#
#   gem install listo-delivery
#   LISTO_SIGNATURE='t=...,v1=...' LISTO_WEBHOOK_SECRETS=whsec_a,whsec_b \
#     ruby verify_webhook_signature.rb < body.json
#
require "listo_delivery"

# The RAW bytes, before any JSON parsing. Parsing and re-serializing changes
# them (key order, escapes, whitespace) and the signature covers what was sent,
# not what your JSON library would have sent.
payload = $stdin.read

begin
  # Checks the signature and the 300 second window in BOTH directions (the
  # replay this scheme exists to stop), then parses the body. `secrets` is a
  # LIST because a rotation has two secrets live at once: accept either while
  # you swap over, then drop the old one. No API key is needed.
  event = ListoDelivery::Webhooks.construct_event(
    payload,
    ENV.fetch("LISTO_SIGNATURE", nil),
    ENV.fetch("LISTO_WEBHOOK_SECRETS").split(",")
  )
rescue ListoDelivery::Error
  # `invalid_signature` (SignatureError) and `invalid_payload` (a signed body
  # that is not a JSON object) are both a ListoDelivery::Error. Answer 400 and
  # stop. Do not parse it, do not act on it, do not log it as an event you
  # received.
  abort("signature rejected")
end

# Events arrive at least once and in no guaranteed order, so treat `id` as the
# thing you deduplicate on and the object's own timestamps as the thing you
# order by. Answer 2xx quickly and do the work afterwards.
puts "#{event.type} #{event.id}"

case event.type
when "delivery.status_changed", "delivery.updated"
  puts "order #{event.data.external_id} is #{event.data.status}"
when "delivery.courier_location"
  puts "courier at #{event.data.location.lat},#{event.data.location.lng}"
else
  # A new type is an additive change, so ignore what you do not recognise
  # rather than failing on it.
  puts "ignored"
end
```

```ruby HTTP (Ruby) theme={null}
# Verifies the signature on an incoming Listo webhook before trusting a byte of
# it. Reads the raw request body on stdin, the way your web framework hands it
# to you, and refuses anything that does not check out.
#
#   LISTO_SIGNATURE='t=...,v1=...' LISTO_WEBHOOK_SECRETS=whsec_a,whsec_b \
#     ruby verify_webhook_signature.rb < body.json
#
require "json"
require "openssl"

# The window a signature stays good for, matching the one Listo signs against.
# It is what stops a request someone captured from being replayed at you later.
TOLERANCE_SECONDS = 300

# `secrets` is a LIST because a rotation has two secrets live at once: accept
# either while you swap over, then drop the old one.
def listo_signature_valid?(payload, header, secrets, now: Time.now)
  match = /\At=(\d+),v1=(\h+)\z/.match(header.to_s)
  return false if match.nil?

  timestamp = match[1].to_i
  signature = match[2]

  # Checked in BOTH directions. A timestamp far in the past is the replay this
  # scheme exists to stop; one far in the future is the same attack against a
  # clock nobody looked at.
  return false if (now.to_i - timestamp).abs > TOLERANCE_SECONDS

  secrets.any? do |secret|
    expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{payload}")
    OpenSSL.secure_compare(expected, signature)
  end
end

# The RAW bytes, before any JSON parsing. Parsing and re-serializing changes
# them (key order, escapes, whitespace) and the signature covers what was sent,
# not what your JSON library would have sent.
payload = $stdin.read
header = ENV.fetch("LISTO_SIGNATURE", nil)
secrets = ENV.fetch("LISTO_WEBHOOK_SECRETS").split(",")

unless listo_signature_valid?(payload, header, secrets)
  # Answer 400 and stop. Do not parse it, do not act on it, do not log it as an
  # event you received.
  abort("signature rejected")
end

event = JSON.parse(payload)

# Events arrive at least once and in no guaranteed order, so treat `id` as the
# thing you deduplicate on and the object's own timestamps as the thing you
# order by. Answer 2xx quickly and do the work afterwards.
puts "#{event["type"]} #{event["id"]}"

case event["type"]
when "delivery.status_changed", "delivery.updated"
  delivery = event["data"]
  puts "order #{delivery["external_id"]} is #{delivery["status"]}"
when "delivery.courier_location"
  location = event.dig("data", "location")
  puts "courier at #{location["lat"]},#{location["lng"]}"
else
  # A new type is an additive change, so ignore what you do not recognise
  # rather than failing on it.
  puts "ignored"
end
```

The secret is the one `listen` printed. Pass it where that sample reads
`LISTO_WEBHOOK_SECRETS` (the sample takes a comma separated list, so a single
secret is a list of one). Three things to know about it:

* **It is made on your computer, for `listen`.** It is not your endpoint's secret
  from the dashboard, and that one never leaves Listo. In development you verify
  with the secret `listen` printed; in production, with your endpoint's. Reading
  both from the same environment variable in your app is the whole difference.
* **It does not change.** It is created the first time and is the same on every
  start, so you set it once. `listo listen --print-secret` prints it and exits,
  which is what you want in a script: `export LISTO_WEBHOOK_SECRET=$(listo listen --print-secret)`.
* **Test and Live each have their own**, and `listo logout` removes the secret
  of the mode it ends.

See [Receive and verify webhooks](https://docs.listo.delivery/en/guides/webhooks.md#verify-before-you-trust)
for what a verifier checks, and [signing](https://docs.listo.delivery/en/concepts/signing.md) for the scheme.

### What `listen` does with your answer

It sends each event once and never retries: a `500` is shown in red so you can
fix your handler and trigger the event again. A reply with a redirect is reported
as the answer it is, not followed. A server that is not running is a line saying
it could not be reached, and `listen` goes on with the next event. A request that
takes longer than 30 seconds is given up on.

To receive only some types, name them:

```bash theme={null}
listo listen --forward-to http://localhost:3000/webhooks/listo --events delivery.status_changed,store.updated
```

A name that is not a published event type is refused, and the error lists the
ones that are.

### When the connection drops

`listen` reconnects by itself and tells you ("Connection lost, reconnecting in 2
s..."). When it is back, it reads the events written meanwhile from
[`GET /v1/events`](https://docs.listo.delivery/api-reference/events/list-your-webhook-events), starting from
the last event it saw, and forwards the ones you did not get, oldest first, each
event once ("Reconnected. Events missed while offline: 3."). The `--events` filter
applies to them too. What it does **not** replay:

* **Anything from before `listen` started.** The first connection catches up
  nothing; it starts from now.
* **Courier positions** (`delivery.courier_location`). The events list leaves
  them out unless you ask for that type, and a position that old is no use.
* **More than 2,000 events at once.** An outage of a day does not queue a day of
  traffic.

If you need the full truth after a long gap, read the deliveries themselves, as
[Receive and verify webhooks](https://docs.listo.delivery/en/guides/webhooks.md#when-you-missed-some) explains.

If you disconnect the computer in the dashboard, or log out elsewhere, the
running `listen` stops with "Session ended." and the command to log in again.

## Send a test event

```bash theme={null}
listo trigger delivery.status_changed
```

```
Sent evt_7hS2kQ9xR4tV6bN1cZ8sLwYe (test: true) to your listen sessions. Add --deliver to also POST it to your webhook URL.
```

`trigger` records a sample event of that type, marked `test: true`, and hands it
to the `listen` sessions of your account. It does not call your webhook URL, so
it needs none to be configured. The eight types are `delivery.status_changed`,
`delivery.updated`, `delivery.courier_location`, `withdrawal.updated`,
`store.updated`, `store.connected`, `delivery.message_skipped` and
`delivery.rated`.

Add `--deliver` to also `POST` it to the webhook URL set in the dashboard for
the mode, the way a real event goes out:

```bash theme={null}
listo trigger delivery.status_changed --deliver
```

```
Sent evt_7hS2kQ9xR4tV6bN1cZ8sLwYe (test: true) to your listen sessions and queued it for your webhook URL.
```

The attempt then shows in [`GET /v1/webhook_deliveries`](https://docs.listo.delivery/api-reference/webhook-deliveries/list-webhook-delivery-attempts).
With `--deliver` and no URL for the mode, the API answers
[`webhook_endpoint_not_configured`](https://docs.listo.delivery/en/errors/webhook_endpoint_not_configured), and
sending more than 30 test events an hour answers
[`test_webhook_limit_reached`](https://docs.listo.delivery/en/errors/test_webhook_limit_reached). It is the same
call as [`POST /v1/test_webhooks`](https://docs.listo.delivery/en/guides/debugging#send-a-test-webhook).

## Follow your API calls

```bash theme={null}
listo logs tail
```

```
Following the request log [TEST]. Press Ctrl+C to stop.
12:04:09  POST    /v1/deliveries  201  412 ms  0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
12:04:30  POST    /v1/deliveries  422  118 ms  validation_failed  5d8e3f2a-6b10-4c53-9a41-0b1f6c9a7e2d
```

One line per `/v1/` call of your account, as it is made, whoever makes it: the
time, the method, the path, the status (green, or red from 400), how long it
took, the error code when it failed and the request id. That id is the one to
quote to support, and the one to look up in
[`GET /v1/request_logs`](https://docs.listo.delivery/api-reference/request-logs/list-your-request-log).

```bash theme={null}
listo logs tail --status failed
listo logs tail --endpoint api/v1/deliveries#create
```

`--status failed` shows only answers from 400 up, `succeeded` the rest, and
`--endpoint` takes the exact value a log row shows. The reads `listo` makes
itself to catch up after a lost connection are hidden, so they do not fill your
log; add `--all` to see them. It reconnects and catches up the way `listen` does.

## Create and read deliveries

```bash theme={null}
listo deliveries create --data @order.json
```

```
Idempotency-Key: 6f1c0f0e-2f1b-4d3e-9f6a-8b7c5d4e3a21
[TEST] Created dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe (LS-2048), status dispatching.
Delivery fee $80.04 MXN paid by the customer.
Follow it at https://app.listo.delivery/t/abc123
Run listo listen to watch it.
```

`order.json` is the body of [`POST /v1/deliveries`](https://docs.listo.delivery/api-reference/deliveries/create-a-delivery)
as it is, and the API checks it: see [Quote and create a delivery](https://docs.listo.delivery/en/guides/quote-and-create.md).
Pass the JSON with `--data @order.json`, with `--data @-` or by piping it in, or
write it inline in `--data`. The command makes an `Idempotency-Key` for the call
and prints it, so sending the same call again never books a second courier. To
send your own, use `--idempotency-key`.

```bash theme={null}
listo deliveries get dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe
listo deliveries cancel dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe --reason store_closed
```

`cancel` takes `--reason` (`customer_canceled`, `store_closed`, `item_unavailable`,
`wrong_address`, `no_longer_needed` or `other`, the default) and `--description`,
a sentence of explanation. Canceling after a courier was already working on the
order is billed as a cancellation fee, as it is through the API.

### Play a courier scenario

```bash theme={null}
listo simulate returned
```

```
Started the returned scenario: dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe (dispatching).
Run listo listen to watch it.
```

`simulate` creates a test delivery with the quickstart's sample drop-off and asks
the simulated courier to play `returned`, `no_courier`, `delayed_pickup` or
`courier_reassigned` (see [test mode](https://docs.listo.delivery/en/concepts/test-mode.md)). It uses your first
test store, or the one you name with `--store st_...`. With `listo listen` running
in another window you watch the events it produces. It runs in test mode only: a
live key is refused before anything is called.

## Live mode

Add `--live` and the command acts on your live account, with real money and real
couriers. Every line that names a mode shows a `[LIVE]` badge instead of `[TEST]`.
Reading is free of ceremony (`whoami`, `deliveries get`, `listen`, `logs tail`).
**Writing asks first**, and the question says exactly what will happen:

```bash theme={null}
listo deliveries create --live --data @order.json
```

```
This will book a courier for "Taquería El Güero" (st_4bR8kM2wQ7xT1nV5cZ9pLdYe) to Av. Chapultepec 123, Guadalajara, delivery fee $89.00 MXN paid by the store, from your live balance. Continue? [y/N]
```

To get that sentence, the command first prices the trip with
[`POST /v1/quotes`](https://docs.listo.delivery/api-reference/quotes/price-a-delivery) for the same store and
drop-off, and on `y` it books **that quote**, so the amount you read is the amount
charged. Any answer but `y` cancels, and nothing is changed.

* `deliveries cancel --live` shows the delivery, its status and what the cancel
  can cost before it asks.
* `trigger --live` says that the event will be recorded on your live account,
  and with `--deliver` that it will reach your live webhook URL.
* `--yes` skips the question, for scripts you have decided to trust. For a
  delivery it still prints the sentence, so the log says what was done.
* With no terminal to ask on (a script, a pipe) and no `--yes`, the command
  refuses with exit code 2 and the line `Add --yes.`. It never proceeds on its own.

## In scripts

Output is JSON when you add `--json` and whenever stdout is not a terminal, so
a pipe always gets something a program can read; colour appears only on a
terminal and `NO_COLOR` turns it off.

```bash theme={null}
listo deliveries get dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe | jq -r .status
listo listen --forward-to http://localhost:3000/webhooks/listo --json
```

A command that prints an API object prints the API's own object, on one line when piped.
`listen` and `logs tail` print one JSON object per line. An error goes to stderr
as one JSON object (`code`, `message`, and `param`, `doc_url`, `request_id` and
`next_step` when there are any), and the human lines that say what happened
(`Ready`, `Sent`, the confirmation sentence) go to stderr too, so stdout carries
nothing but JSON.

| Exit code | Meaning |
| - | - |
| `0` | It worked. |
| `1` | The API refused the call, or the operation failed. The message, code, `doc_url` and `request_id` are the API's own, and the next step is named ("Run listo login"). |
| `2` | The command was typed wrong, or a live write could not be confirmed. |

## Language

The tool speaks English or Spanish. It uses the language you ask for with `--lang es`
or `--lang en`, else `LISTO_LANG`, else your computer's (`LC_ALL`, `LC_MESSAGES`,
`LANG`, and on a Mac its system language), and anything that starts with `es` is
Spanish. Commands, flags and JSON keys stay English. The language also goes to the
API with every call, so an error's `message` arrives in it.

## Environment variables

| Variable | Meaning |
| - | - |
| `LISTO_API_KEY` | An API key (`lst_test_...` or `lst_live_...`), used instead of a saved login. |
| `LISTO_API_URL` | The API address. Default `https://api.listo.delivery`. |
| `LISTO_LOGIN_URL` | The sign-in address. Default: the API address with `api` replaced by `login`. |
| `LISTO_CABLE_URL` | The live connection `listen` and `logs tail` use. Default `wss://cable.listo.delivery/cable`. |
| `LISTO_LANG` | Output language, `es` or `en`. `--lang` wins over it. |
| `LISTO_CONFIG_DIR` | Where the tool keeps its files. Default: your system's config folder, under `listo`. |
| `LISTO_NO_KEYRING` | Set to `1` to keep credentials in the file and never in the system store. |
| `LISTO_NO_UPDATE_NOTIFIER` | Set to `1` to silence the once-a-day notice that a new version exists. |
| `NO_COLOR` | Turns colour off. |

## Keep it up to date

Once a day, on a terminal, `listo` prints one line on stderr when a newer
version exists, naming the command that upgrades it the way you installed it. It
is never printed with `--json` or when the output is piped.

| Installed with | Upgrade with |
| - | - |
| npm | `npm i -g listo-cli` |
| Homebrew | `brew upgrade listo` |
| The install script | `listo update` |

`listo update` replaces the program itself when you installed it with the install
script, after checking the download against Listo's release key and its checksum;
nothing is replaced unless both pass. For an npm or Homebrew copy it prints that
tool's command instead. `listo update --check` only tells you whether a newer
version exists. It never updates in the background, so the tool you are using
does not change under you mid-session.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.