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.
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 toPOST /v1/connect_requestsor 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, theoriginal_bytesit had and the first 64 KB of itstext, 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_bodyandresponse_bodyarenull. - Rows are kept. There is no retention window to race. A row from before
this log was available has no
pathorrequest_id, bothnull.
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.
- The same event. It has the same
idand the byte-identical body, so a receiver that deduplicates onid, 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-Aftersays how many seconds until you can send again. - Never a courier position. A
delivery.courier_locationevent is superseded by the next one within seconds, so asking to resend one is409 event_not_resendable. - Needs an endpoint. With no webhook URL configured for the key’s mode the
answer is
409 webhook_endpoint_not_configured.
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.
202 with the event as your endpoint receives it, and the attempt
shows up in GET /v1/webhook_deliveries as with any other event:
test before you act.
"test": trueis 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.
datais a realistic sample of the type, but the delivery, store, withdrawal or quote it names does not exist, soGET /v1/deliveries/{id}for it answers404. 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, andRetry-Aftersays how many seconds until you can send again. Atypethat is not a published name is422 validation_failedwithparam: "type", and a mode with no webhook URL is409 webhook_endpoint_not_configured.
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.
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
- Reproduce the problem with a test key, so nothing real is at stake.
- Open the call in the request log. The status,
error.codeandmessageof the response body, and your own request body, answer most questions. - If it was an event you were waiting for, find it in
GET /v1/events, then its attempts inGET /v1/webhook_deliveries. - Fix the handler, then resend that event, or send a test webhook, until the
attempt reads
succeeded. - Check the same on the live endpoint with a test webhook before you rely on it.