> ## 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.
> - Every live store carries one Plan Listo, charged to the partner. 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; 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) can act through the same API key at `https://api.listo.delivery/mcp`, an MCP server over Streamable HTTP authenticated with `Authorization: Bearer <API key>`. 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.

# Debugging your integration

> See every call you made, every event Listo emitted and every attempt to deliver it, then resend an event or send a test one.

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](https://partners.listo.delivery), 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.

| You want to know | Read | In the dashboard |
| - | - | - |
| What a call sent and what Listo answered | [`GET /v1/request_logs`](https://docs.listo.delivery/api-reference/request-logs/list-your-request-log) | Request logs |
| What Listo emitted | [`GET /v1/events`](https://docs.listo.delivery/api-reference/events/list-your-webhook-events) | Webhooks |
| Whether it reached your endpoint, and what came back | [`GET /v1/webhook_deliveries`](https://docs.listo.delivery/api-reference/webhook-deliveries/list-webhook-delivery-attempts) | Webhooks |

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](https://docs.listo.delivery/en/errors.md) 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.

```bash theme={null}
curl "https://api.listo.delivery/v1/request_logs?status=failed" \
  -H "Authorization: Bearer $LISTO_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "req_4f9c2b7e1a8d3056b1c2d3e4",
      "method": "POST",
      "path": "/v1/deliveries",
      "endpoint": "api/v1/deliveries#create",
      "status": 422,
      "error_code": "validation_failed",
      "duration_ms": 118,
      "request_id": "0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10",
      "api_key_prefix": "lst_test_Ab1",
      "test": true,
      "created_at": "2026-11-06T16:02:11Z"
    }
  ],
  "has_more": false,
  "cursor": null
}
```

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}`](https://docs.listo.delivery/api-reference/request-logs/retrieve-a-request-log-entry) 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`.

> [!WARNING]
>
> **Bodies carry your customers' data**
>
> A 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`](https://docs.listo.delivery/api-reference/events/list-your-webhook-events) lists what Listo
emitted for you, whether or not your endpoint received it, and
[`GET /v1/events/{id}`](https://docs.listo.delivery/api-reference/events/retrieve-a-webhook-event) 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`](https://docs.listo.delivery/api-reference/webhook-deliveries/list-webhook-delivery-attempts)
lists them.

```json theme={null}
{
  "id": "whd_8d2e5f1a7c3b9046a1b2c3d4",
  "event_id": "evt_9pQ2vX7mKdR4tYb1nZ3sLwCe",
  "event_type": "delivery.status_changed",
  "attempt": 2,
  "resent": false,
  "url": "https://example.com/listo/webhooks",
  "status_code": null,
  "error_message": "execution expired",
  "response_body": null,
  "duration_ms": 5000,
  "succeeded": false,
  "created_at": "2026-11-06T16:04:31Z"
}
```

`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](https://docs.listo.delivery/en/guides/webhooks.md#respond-then-work)).
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`](https://docs.listo.delivery/api-reference/events/send-a-webhook-event-again) 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.

```bash theme={null}
curl -X POST "https://api.listo.delivery/v1/events/evt_9pQ2vX7mKdR4tYb1nZ3sLwCe/resend" \
  -H "Authorization: Bearer $LISTO_API_KEY"
```

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

> [!WARNING]
>
> **A resend is a snapshot, not the current state**
>
> The 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`](https://docs.listo.delivery/api-reference/deliveries/list-deliveries): that is
> always the current truth. See [when you missed some](https://docs.listo.delivery/en/guides/webhooks.md#when-you-missed-some).

## Send a test webhook

You do not need an order to exercise your endpoint.
[`POST /v1/test_webhooks`](https://docs.listo.delivery/api-reference/events/send-a-test-webhook) sends a
synthetic event of any published type to the webhook URL of the key's mode.

```bash theme={null}
curl -X POST "https://api.listo.delivery/v1/test_webhooks" \
  -H "Authorization: Bearer $LISTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "delivery.status_changed"}'
```

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:

```json theme={null}
{
  "id": "evt_3kM7qR2vX9pNdY4tB6nZ1sLw",
  "type": "delivery.status_changed",
  "created_at": "2026-11-06T16:10:02Z",
  "test": true,
  "data": {
    "id": "dlv_test_sample_000000000000",
    "external_id": "order-841",
    "status": "en_route"
  }
}
```

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

| HTTP | `code` | What happened |
| - | - | - |
| 403 | `read_only_key` | A resend or a test webhook was asked with a read-only key. Both are writes, so a read-only key can list and read but not send. Use a key issued without the read-only setting. |
| 404 | `not_found` | The id is not yours, belongs to the other mode, or never existed. A filter that names an event or a delivery that is not yours answers the same. |
| 422 | `validation_failed` | A date filter did not parse, or a test webhook's `type` is not a published event type. `error.param` names it. |
| 409 | `webhook_endpoint_not_configured` | No webhook URL is set for the key's mode, so a resend or a test webhook has nowhere to go. Set one in the dashboard. |
| 409 | `event_not_resendable` | The event is a `delivery.courier_location`, which is never sent again. |
| 429 | `resend_limit_reached` | That event was already resent 5 times in the last hour. Retry once the hour has moved on. |
| 429 | `test_webhook_limit_reached` | 30 test webhooks were already sent in this mode in the last hour. Retry later. |

## In the dashboard

The same records are in the [partner dashboard](https://partners.listo.delivery),
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](https://docs.listo.delivery/en/guides/connect-ai-client.md), 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:

| You ask about | The assistant uses |
| - | - |
| The calls you made | `search_api_logs`, then `get_api_log` for the request and response bodies |
| What Listo emitted | `list_events`, then `get_event` |
| Whether it reached your endpoint | `list_webhook_deliveries` |
| Sending it again | `resend_event` |
| Trying your endpoint without an order | `send_test_webhook` |

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


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