Skip to main content
When something does not work, the question is always one of three. What did my call send and what did Listo answer? Did Listo emit the event I was waiting for? Did it reach my endpoint, and what did my endpoint say? Each has a record you can read yourself, through the API or in the partner dashboard, with nobody at Listo in the middle. All of it belongs to the mode of the key you use: a test key reads test records, a live key reads live ones, and neither can see the other’s. The ids are the same ones the rest of the API uses, and they are opaque: never parse one. Every list is the same shape as the rest of the API: a data array, a has_more boolean and an opaque cursor, newest first, up to 50 rows a page.

Request logs

Every call your keys make to /v1/ is recorded: the method and path, the status we answered, the error.code when it failed, how long it took, which key made it (its first characters, never the secret) and whether it was a test call. Each row carries a request_id, the value of the X-Request-Id header on the response you got, so it is the thing to quote if you ever write to support. That id is Listo’s own: every response carries it in X-Request-Id, and every error repeats it in the body as error.request_id, next to a doc_url that links to the page explaining the error. If you send your own X-Request-Id, it comes back on the response (Listo cleans up characters it does not accept) and is the id stored, so you can correlate with your logs. The id of a load balancer or another service in front of ours is not this one. To find a call from an id you copied out of an error or a log, filter the list with request_id (an exact match) or paste it into the search box on the dashboard’s Request logs page. A tool call made through the MCP server writes the same rows, and each carries the X-Request-Id of the /mcp response you got back. A two-step tool is two /mcp requests with two ids. A preview that calls the API to read your data writes a GET row with the preview’s id; a preview that calls no endpoint writes no row. The confirmation writes the row of the call that did the work, and confirming the same token again returns the first result and writes no row.
The list narrows with status (failed is every answer from 400 up, succeeded is the rest), endpoint (the exact value a row shows, such as api/v1/deliveries#create), request_id (one call, by its exact id), and created_after and created_before, both ISO-8601 instants. A status that is neither value matches nothing, and a timestamp that does not parse is a 422 validation_failed naming the parameter. GET /v1/request_logs/{id} adds the two bodies, request_body and response_body, exactly as they were kept. That is the answer to “why was this refused”: the response body is the error envelope you got, and the request body is what you actually sent, which is often not what you meant to send. What is recorded, and what is not:
  • Calls that identified a key. A call with no key, or a wrong or revoked one, belongs to nobody and is not in your log, and neither is a call refused for rate limiting. If you are certain you made a call and it is missing, one of those is the reason.
  • Keys and one-time links are masked. An API key anywhere in a body reads as its first characters followed by [key masked], and the link in the response to POST /v1/connect_requests or to a Stripe onboarding call reads [redacted: one-time link], because holding that link is the authority to use it. What you received is untouched; only the stored copy is masked. Headers are never kept.
  • A body is cut at 64 KB. A longer one is kept as an object with _listo_truncated: true, the original_bytes it had and the first 64 KB of its text, so you are always told that you are not reading all of it.
  • The three debugging reads keep no bodies. Reading the request log, the events or the webhook deliveries is itself logged, but without bodies: otherwise the log would copy itself every time you read it. Their request_body and response_body are null.
  • Rows are kept. There is no retention window to race. A row from before this log was available has no path or request_id, both null.
Bodies carry your customers’ dataA delivery’s request and response bodies hold the name, phone and address you sent. They are shown to the people on your team who can sign in to the dashboard. Treat what you copy out of them the way you treat the orders themselves.

Events

GET /v1/events lists what Listo emitted for you, whether or not your endpoint received it, and GET /v1/events/{id} reads one. Each item is the webhook envelope exactly as your endpoint is sent it (id, type, created_at, data), so you can compare what you stored with what we meant to tell you. It narrows with type (a published name such as delivery.status_changed), delivery_id (the events about one delivery; an id that is not yours is a 404), and created_after and created_before. delivery.courier_location is left out unless you ask for it with type. It is emitted about every twenty seconds for every trip on the road and is never sent again, so it would bury everything else on the page.

Webhook deliveries

An event is something Listo decided to tell you. A webhook delivery is one attempt to tell you: the URL we posted to, the status your endpoint answered with, the first 500 characters of its body, how long it took, and whether we counted it a success. GET /v1/webhook_deliveries lists them.
status_code is null when nothing answered at all (a timeout, a refused connection, a certificate error), and error_message says which. attempt counts the event’s attempts from 1, including the retries Listo makes on its own when your endpoint fails (see receive and verify webhooks). It narrows with event_id (an event that is not yours is a 404), status (failed or succeeded) and created_after and created_before. The usual order of questions is: find the event in the list, look at its attempts, and read status_code and response_body to see what your endpoint said. A 400 from your own signature check, a 5xx, a timeout and an event with no attempts at all (no endpoint was configured for the mode) are four different problems and the attempt row tells them apart.

Send an event again

POST /v1/events/{id}/resend sends one event to your endpoint once more, with no body. It answers 202 with the event’s envelope, and the send happens right after: the new attempt appears in GET /v1/webhook_deliveries with resent: true, numbered after the event’s last one.
What a resend is, so you can rely on it:
  • The same event. It has the same id and the byte-identical body, so a receiver that deduplicates on id, as it should, treats it as the event it already saw. To exercise your handler again, clear that record first.
  • Signed now. The signature carries the current time and the current secret, so it verifies even when the event is days old.
  • Sent to the current URL. If you changed your endpoint since the event happened, the resend goes to the new one.
  • Once. There is no retry behind it. If your endpoint fails, you see one failed attempt and you decide what to do next.
  • Limited. An event can be resent 5 times an hour. The sixth is refused with 429 resend_limit_reached, which is retryable once the hour has passed. Retry-After says how many seconds until you can send again.
  • Never a courier position. A delivery.courier_location event is superseded by the next one within seconds, so asking to resend one is 409 event_not_resendable.
  • Needs an endpoint. With no webhook URL configured for the key’s mode the answer is 409 webhook_endpoint_not_configured.
A resend is a snapshot, not the current stateThe body is the snapshot from the moment the event happened. Resending a day-old delivery.status_changed delivers a status the order has long since moved past. Use a resend to debug your endpoint or to recover one event you know you dropped. To catch up on where things stand after an outage, read the deliveries back with GET /v1/deliveries: that is always the current truth. See when you missed some.

Send a test webhook

You do not need an order to exercise your endpoint. POST /v1/test_webhooks sends a synthetic event of any published type to the webhook URL of the key’s mode.
It answers 202 with the event as your endpoint receives it, and the attempt shows up in GET /v1/webhook_deliveries as with any other event:
Four things to know about a test event. The first is on you: branch on test before you act.
  • "test": true is on the envelope. A real event does not carry the field at all. Branch on it before you do anything with consequences.
  • Its ids resolve to nothing. data is a realistic sample of the type, but the delivery, store, withdrawal or quote it names does not exist, so GET /v1/deliveries/{id} for it answers 404. A handler that reads the object back, as it should, will find nothing, which is the proof that it is a test.
  • It works in both modes. A test key sends to your test endpoint and a live key sends to your live one, so you can check a production endpoint without placing an order. That live endpoint does receive the test event, so make sure it is ignored there.
  • It is limited. 30 test events an hour per mode, then 429 test_webhook_limit_reached, and Retry-After says how many seconds until you can send again. A type that is not a published name is 422 validation_failed with param: "type", and a mode with no webhook URL is 409 webhook_endpoint_not_configured.
A test event is listed in GET /v1/events like any other, with test: true.

What these endpoints refuse with

In the dashboard

The same records are in the partner dashboard, for the mode you have selected.
  • Request logs lists your calls with the same filters, and each call opens to its request and response bodies.
  • Webhooks keeps the endpoint settings and shows each attempt. Resend sits on every attempt of an event that can be resent, and Send test webhook sends a synthetic event to the endpoint above it.
Both of those buttons do what the endpoints do, with the same limits.

Debug with your AI assistant

If you have connected an AI assistant, it can read all three records itself, so you can ask in plain words: “why did my last booking fail?”, “did Listo send the delivered event for that order?”, “what did my endpoint answer?”. It uses the same records and the same limits as the endpoints above: resend_event and send_test_webhook act at once: unlike a booking or a withdrawal they move no money, so the assistant does not ask you to confirm them. They keep the limits described above, and the attempt they cause is read with list_webhook_deliveries. When an assistant is told to wait because of a limit, the error carries retry_after_seconds, the seconds until it can try again.

A way to work

  1. Reproduce the problem with a test key, so nothing real is at stake.
  2. Open the call in the request log. The status, error.code and message of the response body, and your own request body, answer most questions.
  3. If it was an event you were waiting for, find it in GET /v1/events, then its attempts in GET /v1/webhook_deliveries.
  4. Fix the handler, then resend that event, or send a test webhook, until the attempt reads succeeded.
  5. Check the same on the live endpoint with a test webhook before you rely on it.