Skip to main content
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). 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.
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:
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:

Log in

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

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.
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 for what a verifier checks, and signing 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:
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, 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 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

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:
The attempt then shows in GET /v1/webhook_deliveries. With --deliver and no URL for the mode, the API answers webhook_endpoint_not_configured, and sending more than 30 test events an hour answers test_webhook_limit_reached. It is the same call as POST /v1/test_webhooks.

Follow your API calls

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

order.json is the body of POST /v1/deliveries as it is, and the API checks it: see Quote and create a delivery. 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.
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

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). 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:
To get that sentence, the command first prices the trip with POST /v1/quotes 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.
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.

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

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