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

# Create a delivery

> Dispatches a delivery.

`Idempotency-Key` is required. Send a value you generate per order, and
retry the same call with the same key if you never saw a response: a
replay within 24 hours returns the original response, status code
included, without dispatching a second courier or debiting a second
time. The same key with a different body is a `409`, and so is a
repeat sent while the original request is still being processed
(`idempotency_conflict` both times). That repeat created nothing and
does not mean the original failed: once the original has answered,
the same request with the same key replays its answer.

`dropoff.phone` is E.164. A Mexican number is +52 followed by its 10 digits (+52 1 followed by 10 is accepted as WhatsApp writes it); a US or Canadian number is +1 followed by its 10 digits; a number from any other country is accepted with 8 to 15 digits in total. Anything else is refused with `validation_failed`.

`external_id` is your own order reference. It is unique per key world,
echoed in every payload and webhook, and filterable on the list, so you
can reconcile without ever storing our ids.

`pickup_ready_at` schedules the pickup for a future moment, between 10
minutes and 29 days out. Absent, the order dispatches now.

How the customer pays is `payment_method`. `cash` has the courier
collect at the door, up to a cap. `card` mints a branded payment link
in `payment.url`, which Listo sends your customer itself by WhatsApp
(and by SMS when the WhatsApp does not reach them), so you do not need
to send it; the order holds at `awaiting_payment` until the customer
pays. `prepaid` means you have
already been paid and the order dispatches immediately.

On a `prepaid` create that binds no quote, `business_fee_share` is
forced to 100 and `business_fee_cap_cents` is ignored, whatever you
send: you have already collected from your customer, so the whole fee
is yours. A create that binds a quote is held to the split the quote
was priced with, so request the quote with `payment_method: prepaid`
or with no `payment_method`: either is always priced at share 100 and
binds a `prepaid` create. Any other quote is refused with `422
validation_failed`, and the first thing that does not match is the one
named. A quote requested with another `payment_method` (`cash` or
`card`) is refused naming `payment_method`. A quote whose split is not
the one a `prepaid` order has (share 100, no ceiling) is refused naming
`business_fee_share` or `business_fee_cap_cents`, and `error.message`
says why and what to send instead: a new quote requested with
`payment_method: prepaid`.

`vehicle` is `moto` (the default) or `car`, and `manifest.items[].size`
is `small` (the default), `medium`, `large` or `xlarge`. A trip goes by
car when you ask for one or when any item is sized `xlarge`, and for no
other reason. A car is a sedan, never a van, can take longer to assign,
and adds a surcharge inside `fee_cents`. The delivery's `vehicle` says
what the trip was priced and dispatched as; `courier.vehicle_type` is
what actually arrived, which can be a car on a `moto` trip without the
fee changing.

A create that binds a quote is held to the vehicle the quote was priced
as. Leave `vehicle` out and it inherits the quote's. Say `moto` against
a car quote, `car` against a moto quote, or send an item sized `xlarge`
against a moto quote, and it is refused with `422 validation_failed`
naming `vehicle` (or `manifest.items` when the sizes are what
disagree). Re-quote with the cart you mean to send.

`simulate` rehearses what goes wrong, and it works on a test key only.
A live key that sends it is refused with `422 validation_failed`
naming `simulate`, and so is a value that is not one of the three
below. Leave it out and the delivery plays the normal happy path.

- `returned`: the courier reaches the door, cannot hand the order
  over and brings it back. The delivery goes to `returning` and ends
  at `returned` with `failure_reason: customer_unavailable`, and the
  return is billed as a live one is: one `return_fee` entry in
  `charges` covers both legs (see `charges`).
- `no_courier`: the create answers the same `409
  no_couriers_available` a live create answers when every courier is
  busy, and nothing is created, charged or recorded under your
  `Idempotency-Key`. There is no delivery to retry; repeat the call.
- `delayed_pickup`: the courier is late. Pickup happens about 25
  minutes after the create and the drop-off about 5 minutes after
  that, counted from `pickup_ready_at` instead when you sent one (the
  delivery waits in `scheduled` until then). It goes through the
  same statuses as the normal run.

In test mode a delivery may go from `dispatching` straight to
`picking_up` without ever showing a `courier_assigned` status, in
every scenario. Do not wait for it: `courier_assigned_at` and the
courier object are filled in.

There is no scenario for a courier who gives up before pickup. The
same `Idempotency-Key` with a different `simulate` is a `409
idempotency_conflict`, like any other changed body, once a request has
used the key (it created a delivery, or it is still being processed).
A request refused before it created anything, such as the `no_courier`
409, leaves the key free: send it again with a different `simulate`,
or none, and it is a new request. A `returned` delivery cannot be
retried (`409 not_retriable`): its order ended.

A cart too large for one courier trip is refused with `422
order_too_large`, naming `manifest.items`, and nothing is dispatched or
debited. Split it across two deliveries.

Listo delivers in Mexico. A store that turns out to be somewhere else is
refused with `422 unsupported_country` and nothing is dispatched or
debited; it is final rather than retryable, and the same store will be
refused every time.

A store whose pickup point we cannot currently vouch for is refused
with `409 pickup_point_pending`, and nothing is dispatched or debited:
until it is confirmed a courier could be sent to the wrong place.
Usually the store's address has just changed and the confirmation has
not landed yet, but not always, so do not read it as "the merchant
moved". Unlike the refusal above it this one IS retryable, because
nothing about your request is wrong and the identical call succeeds once
the pickup point is confirmed.

Retry it on a minutes-scale backoff, for a bounded number of attempts,
and then put it in front of a human rather than keep polling. Most moves
land within minutes on their own; a few are held up by something only
our staff can clear and take considerably longer, and this response does
not tell the two apart. There is no `Retry-After` on it for that reason:
we would be inventing the number. The restaurant can also unblock it
themselves from their own Listo console, which is usually the faster of
the two.

When every courier near the trip is busy as the create prices it, the
create is refused with `409 no_couriers_available`, and nothing is
dispatched or debited. It is retryable: repeat the identical request,
with the same `Idempotency-Key`, after a few minutes, for a bounded
number of attempts. Nothing was recorded under that key, so the repeat
creates the order once a courier is free rather than replaying the
refusal.

A live store whose Plan Listo is not paid up is refused with `402
plan_required`, and nothing is dispatched or debited. It is a different
refusal from `402 insufficient_balance`, which is about your balance:
read `error.code`, never the status alone. `error.message` names the
reason (no active plan, a charge that is pending, a plan that ended on a
given date, a plan whose included deliveries for the cycle are all used,
or a store that cannot request couriers right now), and it
is not `retryable`. See
[Plan and billing](https://docs.listo.delivery/en/guides/plan-and-billing).

A dropoff beyond the store's 15 km service area is refused with `422
out_of_service_area`, which is final. The courier network can also
refuse the distance of a dropoff at the very edge of that circle, and
the answer is the same `422`, whether it refuses on the price or on the
create itself. On the create it comes after the delivery was recorded,
so the delivery stays, ended at `no_courier` with `failure_reason:
delivery_failed` under your `external_id`, its fee debit returned in
full and nothing else charged; replaying the request answers the same
`422`. Send that order to another address under a new `external_id`.




## OpenAPI

````yaml /openapi/en.yaml post /v1/deliveries
openapi: 3.1.0
info:
  title: Listo Delivery API
  version: 1.34.0
  description: |
    Quote a delivery, dispatch it, and follow it to the door.

    Every request authenticates with a bearer API key. The key itself decides
    the world it acts in: a test key dispatches simulated couriers and moves no
    money, a live key dispatches real ones. A key can also be read-only: it
    may call GET and HEAD and nothing else, and any other call answers
    `403 read_only_key`. Money is always in Mexican pesos and
    always in integer cents, and every timestamp is ISO-8601 in UTC.

    Ids are opaque prefixed strings (`st_` a store, `qt_` a quote, `dlv_` a
    delivery, `wd_` a withdrawal, `evt_` an event, `le_` a ledger entry,
    `whd_` a webhook delivery attempt, `req_` a logged request). Treat
    them as strings of unbounded length and never parse them.

    Changes to v1 are additive: new fields and new enum values can appear, so
    ignore what you do not recognise rather than failing on it.
servers:
  - url: https://api.listo.delivery
security: []
tags:
  - name: Authentication
    description: Checking that a key works before you build against it.
  - name: Stores
    description: The pickup locations you dispatch from.
  - name: Account connection
    description: Connecting a restaurant's existing Listo account to you.
  - name: Quotes
    description: What a delivery costs, priced and held.
  - name: Deliveries
    description: Dispatching an order and following it to the door.
  - name: Balance
    description: The prepaid balance your deliveries draw on.
  - name: Withdrawals
    description: Moving money out of that balance to your bank.
  - name: Card onboarding
    description: Connecting the account that takes card payments.
  - name: Events
    description: >-
      The events Listo sent you, resending one, and sending a test event to your
      endpoint.
  - name: Webhook deliveries
    description: >-
      Every attempt Listo made to send an event to your endpoint, and what came
      back.
  - name: Request logs
    description: >-
      Every call your keys made to this API, with what you sent and what we
      answered.
paths:
  /v1/deliveries:
    post:
      tags:
        - Deliveries
      summary: Create a delivery
      description: >
        Dispatches a delivery.


        `Idempotency-Key` is required. Send a value you generate per order, and

        retry the same call with the same key if you never saw a response: a

        replay within 24 hours returns the original response, status code

        included, without dispatching a second courier or debiting a second

        time. The same key with a different body is a `409`, and so is a

        repeat sent while the original request is still being processed

        (`idempotency_conflict` both times). That repeat created nothing and

        does not mean the original failed: once the original has answered,

        the same request with the same key replays its answer.


        `dropoff.phone` is E.164. A Mexican number is +52 followed by its 10
        digits (+52 1 followed by 10 is accepted as WhatsApp writes it); a US or
        Canadian number is +1 followed by its 10 digits; a number from any other
        country is accepted with 8 to 15 digits in total. Anything else is
        refused with `validation_failed`.


        `external_id` is your own order reference. It is unique per key world,

        echoed in every payload and webhook, and filterable on the list, so you

        can reconcile without ever storing our ids.


        `pickup_ready_at` schedules the pickup for a future moment, between 10

        minutes and 29 days out. Absent, the order dispatches now.


        How the customer pays is `payment_method`. `cash` has the courier

        collect at the door, up to a cap. `card` mints a branded payment link

        in `payment.url`, which Listo sends your customer itself by WhatsApp

        (and by SMS when the WhatsApp does not reach them), so you do not need

        to send it; the order holds at `awaiting_payment` until the customer

        pays. `prepaid` means you have

        already been paid and the order dispatches immediately.


        On a `prepaid` create that binds no quote, `business_fee_share` is

        forced to 100 and `business_fee_cap_cents` is ignored, whatever you

        send: you have already collected from your customer, so the whole fee

        is yours. A create that binds a quote is held to the split the quote

        was priced with, so request the quote with `payment_method: prepaid`

        or with no `payment_method`: either is always priced at share 100 and

        binds a `prepaid` create. Any other quote is refused with `422

        validation_failed`, and the first thing that does not match is the one

        named. A quote requested with another `payment_method` (`cash` or

        `card`) is refused naming `payment_method`. A quote whose split is not

        the one a `prepaid` order has (share 100, no ceiling) is refused naming

        `business_fee_share` or `business_fee_cap_cents`, and `error.message`

        says why and what to send instead: a new quote requested with

        `payment_method: prepaid`.


        `vehicle` is `moto` (the default) or `car`, and `manifest.items[].size`

        is `small` (the default), `medium`, `large` or `xlarge`. A trip goes by

        car when you ask for one or when any item is sized `xlarge`, and for no

        other reason. A car is a sedan, never a van, can take longer to assign,

        and adds a surcharge inside `fee_cents`. The delivery's `vehicle` says

        what the trip was priced and dispatched as; `courier.vehicle_type` is

        what actually arrived, which can be a car on a `moto` trip without the

        fee changing.


        A create that binds a quote is held to the vehicle the quote was priced

        as. Leave `vehicle` out and it inherits the quote's. Say `moto` against

        a car quote, `car` against a moto quote, or send an item sized `xlarge`

        against a moto quote, and it is refused with `422 validation_failed`

        naming `vehicle` (or `manifest.items` when the sizes are what

        disagree). Re-quote with the cart you mean to send.


        `simulate` rehearses what goes wrong, and it works on a test key only.

        A live key that sends it is refused with `422 validation_failed`

        naming `simulate`, and so is a value that is not one of the three

        below. Leave it out and the delivery plays the normal happy path.


        - `returned`: the courier reaches the door, cannot hand the order
          over and brings it back. The delivery goes to `returning` and ends
          at `returned` with `failure_reason: customer_unavailable`, and the
          return is billed as a live one is: one `return_fee` entry in
          `charges` covers both legs (see `charges`).
        - `no_courier`: the create answers the same `409
          no_couriers_available` a live create answers when every courier is
          busy, and nothing is created, charged or recorded under your
          `Idempotency-Key`. There is no delivery to retry; repeat the call.
        - `delayed_pickup`: the courier is late. Pickup happens about 25
          minutes after the create and the drop-off about 5 minutes after
          that, counted from `pickup_ready_at` instead when you sent one (the
          delivery waits in `scheduled` until then). It goes through the
          same statuses as the normal run.

        In test mode a delivery may go from `dispatching` straight to

        `picking_up` without ever showing a `courier_assigned` status, in

        every scenario. Do not wait for it: `courier_assigned_at` and the

        courier object are filled in.


        There is no scenario for a courier who gives up before pickup. The

        same `Idempotency-Key` with a different `simulate` is a `409

        idempotency_conflict`, like any other changed body, once a request has

        used the key (it created a delivery, or it is still being processed).

        A request refused before it created anything, such as the `no_courier`

        409, leaves the key free: send it again with a different `simulate`,

        or none, and it is a new request. A `returned` delivery cannot be

        retried (`409 not_retriable`): its order ended.


        A cart too large for one courier trip is refused with `422

        order_too_large`, naming `manifest.items`, and nothing is dispatched or

        debited. Split it across two deliveries.


        Listo delivers in Mexico. A store that turns out to be somewhere else is

        refused with `422 unsupported_country` and nothing is dispatched or

        debited; it is final rather than retryable, and the same store will be

        refused every time.


        A store whose pickup point we cannot currently vouch for is refused

        with `409 pickup_point_pending`, and nothing is dispatched or debited:

        until it is confirmed a courier could be sent to the wrong place.

        Usually the store's address has just changed and the confirmation has

        not landed yet, but not always, so do not read it as "the merchant

        moved". Unlike the refusal above it this one IS retryable, because

        nothing about your request is wrong and the identical call succeeds once

        the pickup point is confirmed.


        Retry it on a minutes-scale backoff, for a bounded number of attempts,

        and then put it in front of a human rather than keep polling. Most moves

        land within minutes on their own; a few are held up by something only

        our staff can clear and take considerably longer, and this response does

        not tell the two apart. There is no `Retry-After` on it for that reason:

        we would be inventing the number. The restaurant can also unblock it

        themselves from their own Listo console, which is usually the faster of

        the two.


        When every courier near the trip is busy as the create prices it, the

        create is refused with `409 no_couriers_available`, and nothing is

        dispatched or debited. It is retryable: repeat the identical request,

        with the same `Idempotency-Key`, after a few minutes, for a bounded

        number of attempts. Nothing was recorded under that key, so the repeat

        creates the order once a courier is free rather than replaying the

        refusal.


        A live store whose Plan Listo is not paid up is refused with `402

        plan_required`, and nothing is dispatched or debited. It is a different

        refusal from `402 insufficient_balance`, which is about your balance:

        read `error.code`, never the status alone. `error.message` names the

        reason (no active plan, a charge that is pending, a plan that ended on a

        given date, a plan whose included deliveries for the cycle are all used,

        or a store that cannot request couriers right now), and it

        is not `retryable`. See

        [Plan and
        billing](https://docs.listo.delivery/en/guides/plan-and-billing).


        A dropoff beyond the store's 15 km service area is refused with `422

        out_of_service_area`, which is final. The courier network can also

        refuse the distance of a dropoff at the very edge of that circle, and

        the answer is the same `422`, whether it refuses on the price or on the

        create itself. On the create it comes after the delivery was recorded,

        so the delivery stays, ended at `no_courier` with `failure_reason:

        delivery_failed` under your `external_id`, its fee debit returned in

        full and nothing else charged; replaying the request answers the same

        `422`. Send that order to another address under a new `external_id`.
      operationId: createDelivery
      parameters:
        - name: Idempotency-Key
          in: header
          description: >-
            Required: a call without one is refused with `validation_failed`,
            naming this header. Generate a NEW key for each logical call (a UUID
            is a good one), at most 255 characters, and send the SAME key when
            you retry that call after a timeout or a dropped connection. The
            same key with the same body returns the ORIGINAL response, status
            code included, instead of doing the work again, for 24 hours. The
            same key with a different body is refused with `409
            idempotency_conflict`, and so is a retry that arrives while the
            first request is still being processed (try again shortly). A key
            belongs to your account and to the mode of the API key that sent it
            (live and test keys never meet), so the same text from two accounts,
            or from a live and a test key, is two different keys.
          required: true
          schema:
            type: string
          example: order-841-attempt-1
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDeliveryRequest'
            example:
              store_id: st_4bR8kM2wQ7xT1nV5cZ9pLdYe
              external_id: order-841
              payment_method: prepaid
              order_value_cents: 19600
              business_fee_share: 100
              dropoff:
                name: Ana Torres
                phone: '+528112345678'
                street: Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116
                lat: 20.709395
                lng: -103.413393
                apt_unit: Depto 4B
                notes: Torre B, timbre 4
              handoff: leave_at_door
              manifest:
                items: []
              verification:
                dropoff_pincode: true
              pickup_ready_at: '2026-07-29T12:16:40Z'
              quote_id: qt_5nW3jP8vT2xQ6bM9cZ1sRdKe
              business_fee_cap_cents: 3000
              tip_cents: 500
              vehicle: car
        required: true
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Delivery'
              example:
                id: dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe
                external_id: order-841
                code: LS-2001
                status: dispatching
                store_id: st_4bR8kM2wQ7xT1nV5cZ9pLdYe
                test: true
                payment:
                  method: prepaid
                  status: paid
                  url: null
                  cash_to_collect_cents: null
                  paid_at: '2026-07-29T09:16:40Z'
                order_value_cents: 19600
                fee_cents: 7424
                business_fee_cents: 7424
                customer_fee_cents: 0
                currency: MXN
                quote_id: null
                pickup:
                  name: Tortería La Mera Mera
                  street: Av. Juan Palomar y Arias 765, Zapopan, Jalisco 45110
                  lat: 20.691145
                  lng: -103.426885
                  notes: null
                dropoff:
                  name: Ana Torres
                  phone: '+528112345678'
                  street: Blvd. Puerta de Hierro 4965, Zapopan, Jalisco 45116
                  apt_unit: null
                  lat: 20.709395
                  lng: -103.413393
                  notes: null
                  handoff: meet_at_door
                manifest: null
                courier: null
                pickup_eta_at: '2026-07-29T09:24:30Z'
                dropoff_eta_at: '2026-07-29T09:50:38Z'
                tracking_url: https://app.listo.delivery/t/abc123
                verification: null
                proof: null
                cancellation: null
                failure_reason: null
                support_reference: 2080F
                pickup_ready_at: null
                requested_at: '2026-07-29T09:16:40Z'
                courier_assigned_at: null
                picked_up_at: null
                delivered_at: null
                canceled_at: null
                returned_at: null
                created_at: '2026-07-29T09:16:40Z'
                updated_at: '2026-07-29T09:16:40Z'
                charges: []
                tip_cents: 0
                origin: api
                refunds: []
                refunded_cents: 0
                refundable_cents: 0
                vehicle: moto
                return_verification:
                  pincode: null
                proofs:
                  pickup: null
                  dropoff: null
                  return: null
        '400':
          description: >-
            A short-lived handle you sent is no longer good: a quote that
            expired or was already spent (`quote_expired`, `quote_consumed`),
            which you quote again, or a connection code past its window
            (`code_expired`), whose store was already connected when the owner
            approved and is on `GET /v1/stores`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: quote_consumed
                  message: >-
                    That quote was already used for a delivery. Request a new
                    quote.
                  param: null
                  retryable: false
                  doc_url: https://docs.listo.delivery/en/errors/quote_consumed
                  request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '401':
          description: >-
            The API key is missing, revoked, or not valid for this mode
            (`invalid_api_key`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: invalid_api_key
                  message: The API key is missing, revoked, or not valid for this mode.
                  param: null
                  retryable: false
                  doc_url: https://docs.listo.delivery/en/errors/invalid_api_key
                  request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '402':
          description: >-
            The prepaid balance does not cover this request
            (`insufficient_balance`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                plan_required:
                  value:
                    error:
                      code: plan_required
                      message: This store has no active plan.
                      param: null
                      retryable: false
                      doc_url: https://docs.listo.delivery/en/errors/plan_required
                      request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
                insufficient_balance:
                  value:
                    error:
                      code: insufficient_balance
                      message: The account balance does not cover this request.
                      param: null
                      retryable: false
                      doc_url: >-
                        https://docs.listo.delivery/en/errors/insufficient_balance
                      request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '403':
          description: >-
            The key is valid and is read-only (`read_only_key`), so it can read
            but not create, change or cancel anything. Use a key issued without
            the read-only setting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                read_only_key:
                  value:
                    error:
                      code: read_only_key
                      message: >-
                        This API key is read-only. Use a key without the
                        read-only setting to create, change or cancel anything.
                      param: null
                      retryable: false
                      doc_url: https://docs.listo.delivery/en/errors/read_only_key
                      request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '404':
          description: >-
            No object with that id exists for this key (`not_found`). Another
            account's objects are indistinguishable from ones that never
            existed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: not_found
                  message: No resource with that id exists for this API key.
                  param: null
                  retryable: false
                  doc_url: https://docs.listo.delivery/en/errors/not_found
                  request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '409':
          description: >-
            The request conflicts with the state of the world. The object is no
            longer in a state that allows this (`not_cancellable`,
            `not_reschedulable`, `not_retriable`, `not_editable`,
            `not_refundable`, `not_reactivatable`, `plan_ended`), the
            cancellation did not go through (`cancel_failed`), the plan's charge
            is still with the bank (`charge_in_progress`), the idempotency key
            was reused with a different body (`idempotency_conflict`, which no
            retry fixes), the idempotency key is still held by the request that
            first sent it (`idempotency_conflict`, and once that request has
            answered the same call with the same key replays its answer), there
            is no webhook URL configured for the key's mode so there is nowhere
            to send (`webhook_endpoint_not_configured`), the event is a courier
            position, which is never sent again (`event_not_resendable`), the
            email already has a Listo account (`account_exists`), the store's
            pickup point is still being moved (`pickup_point_pending`), or no
            courier is free for this trip right now (`no_couriers_available`).
            Worth repeating unchanged after a few minutes: `cancel_failed`,
            `charge_in_progress`, `pickup_point_pending`,
            `no_couriers_available`, and a call refused because its idempotency
            key is still held (repeat it with the same key). Every other refusal
            here needs the request or the object to change first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: idempotency_conflict
                  message: That idempotency key was already used with a different body.
                  param: null
                  retryable: false
                  doc_url: https://docs.listo.delivery/en/errors/idempotency_conflict
                  request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '422':
          description: >-
            The request was understood and refused. `error.code` says why and
            `error.param` names the offending field when one applies.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: address_undeliverable
                  message: The dropoff address was rejected as undeliverable.
                  param: null
                  retryable: false
                  doc_url: https://docs.listo.delivery/en/errors/address_undeliverable
                  request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
        '503':
          description: >-
            Dispatch is temporarily unavailable (`provider_unavailable`). Retry
            shortly.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: provider_unavailable
                  message: >-
                    Dispatch is temporarily unavailable. Nothing was created.
                    Retry.
                  param: null
                  retryable: true
                  doc_url: https://docs.listo.delivery/en/errors/provider_unavailable
                  request_id: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
      security:
        - BearerAuth: []
components:
  schemas:
    CreateDeliveryRequest:
      type: object
      description: The order to dispatch.
      properties:
        store_id:
          type: string
          description: The `st_` id of the store the courier collects from.
        quote_id:
          type: string
          description: |
            The `qt_` id of a quote to book at exactly its price. Without one
            the delivery is priced as it is created.
        external_id:
          type: string
          maxLength: 255
          description: Your own reference for this order. Echoed back as `external_id`.
        payment_method:
          type: string
          enum:
            - prepaid
            - cash
            - card
          description: How the customer pays.
        order_value_cents:
          type: integer
          minimum: 0
          description: The value of the order, in centavos.
        business_fee_share:
          type: integer
          minimum: 0
          maximum: 100
          description: |
            The percent of the delivery fee the store pays. Defaults to the
            store's own, and is forced to `100` on a `prepaid` order.
        business_fee_cap_cents:
          type: integer
          minimum: 0
          description: The most the store pays toward the fee, in centavos.
        tip_cents:
          type: integer
          minimum: 0
          maximum: 100000
          description: >-
            A tip for the courier, in centavos, up to 100000. Allowed on a
            `prepaid` order only.
        dropoff:
          type: object
          description: Where the order goes, and who receives it.
          properties:
            name:
              type: string
              description: Who receives the order.
            phone:
              type: string
              description: >-
                A phone the courier can call, any format that resolves to an
                E.164 number.
            street:
              type: string
              description: The full street address, in one string.
            apt_unit:
              type: string
            lat:
              type: number
              format: double
              minimum: -90
              maximum: 90
              description: Latitude. Send it together with `lng`, or neither.
            lng:
              type: number
              format: double
              minimum: -180
              maximum: 180
              description: Longitude. Send it together with `lat`, or neither.
            notes:
              type: string
              maxLength: 280
              description: Instructions for the courier at the door, up to 280 characters.
          required:
            - name
            - phone
            - street
        handoff:
          type: string
          enum:
            - meet_at_door
            - leave_at_door
            - meet_outside
          description: Where the courier hands the order over. Defaults to `meet_at_door`.
        manifest:
          $ref: '#/components/schemas/Manifest'
        verification:
          type: object
          description: Proof the customer must give the courier.
          properties:
            dropoff_pincode:
              type: boolean
              description: True to require a code at the door. Defaults to false.
        pickup_ready_at:
          type: string
          format: date-time
          description: |
            When the order will be ready, ISO-8601 in UTC, between 10 minutes
            and 29 days from now. Leave it out to dispatch now.
        vehicle:
          type: string
          enum:
            - moto
            - car
          description: Defaults to `moto`, or to the quote's when you book one.
        simulate:
          type: string
          enum:
            - returned
            - no_courier
            - delayed_pickup
          description: |
            Test keys only: the scenario the simulated courier plays. A live
            key that sends it is refused.
      required:
        - store_id
        - external_id
        - payment_method
        - order_value_cents
        - dropoff
    Delivery:
      type: object
      description: |
        The shape of a delivery: every field the object can carry, with the
        nullability each one really has. It is the response of every operation
        that answers a delivery (the create, the fetch, a list row, the
        reschedule, the cancel, the retry and the refund) and the `data` of
        every full delivery event, so ONE type holds all of them.

        Every operation returns the whole object. A field that a given call
        cannot yet have is present and null rather than missing: a create
        response has a null `courier`, because no courier has been assigned
        yet, and a reschedule only ever answers with a still-scheduled order,
        so its `delivered_at` is null.

        Money is integer centavos, timestamps are ISO-8601 in UTC, and every
        enum below is ours rather than a passthrough of anyone else's.
      properties:
        id:
          type: string
          description: The delivery id, `dlv_` prefixed.
          example: dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe
        external_id:
          type:
            - string
            - 'null'
          description: Your own order reference, echoed back exactly as you sent it.
          example: order-841
        origin:
          type: string
          enum:
            - api
            - console
            - whatsapp
          description: |
            Which lane created the delivery. `api` is one you created through
            `POST /v1/deliveries`. `console` is one the store dispatched itself
            from the Listo merchant console, which a store can do only at the
            `full` access tier. `whatsapp` is one a customer ordered from the
            store in a chat, which the store then accepted; no operator typed
            it anywhere.

            Always present, never null, and fixed for the life of the delivery.
            A console order carries a null `external_id`, but read this field
            rather than that null: the missing reference is a consequence of the
            lane, not a way of identifying it.
          example: api
        code:
          type: string
          description: |
            The human order code the courier and our support quote. Unique per
            store, not globally, so never address an order by it.
          example: LS-2048
        status:
          type: string
          enum:
            - awaiting_payment
            - awaiting_location
            - scheduled
            - dispatching
            - courier_assigned
            - picking_up
            - en_route
            - returning
            - delivered
            - returned
            - canceled
            - no_courier
          description: |
            `delivered`, `returned` and `canceled` are terminal. `no_courier` is
            a dead end you can retry or cancel. A `returned` delivery is this
            same object throughout; there is no second delivery for the return
            leg.

            `returning`: the drop-off failed and the courier is bringing the
            order back to the store; it ends in `returned`. It is NOT terminal,
            and it is where a failed delivery now spends the minutes between the
            courier giving up at the customer's door and the order actually
            arriving back. `returned` means the order is back. Treat `returning`
            as a live delivery: the courier is still moving and
            `delivery.courier_location` events keep arriving for it.

            The return charge is posted when the delivery enters `returning`,
            not when it reaches `returned`, so `charges` is already complete on
            the `returning` event.

            `awaiting_location`: an order the store took through Listo's console
            or its WhatsApp assistant, held for the drop-off location. No
            courier is requested yet. The customer confirms the location on a
            map link or by sharing it in the WhatsApp chat, or the store
            confirms it from its console. The delivery then moves to
            `scheduled` or `dispatching`; to `no_courier` when a courier
            cannot be requested for it (the location is outside where the
            store delivers, or the store itself cannot request a courier right
            now); or to `canceled` if the order is canceled or the store
            changes it to a pickup at the counter. A delivery you create
            through this API never sits in it.
        store_id:
          type: string
          description: The `st_` id of the store the courier collects from.
        test:
          type: boolean
          description: |
            Whether this order lives in the sandbox. Derived from the store, so
            there is no field to set: a test key can only reach test stores.
        payment:
          type: object
          description: How the customer pays, and where that got to.
          properties:
            method:
              type: string
              enum:
                - prepaid
                - cash
                - card
            status:
              type: string
              enum:
                - unpaid
                - awaiting_payment
                - paid
                - partially_refunded
                - refunded
            url:
              type:
                - string
                - 'null'
              format: uri
              description: |
                The branded payment link for a card order still awaiting
                payment. Listo sends it to your customer itself, by WhatsApp
                and by SMS when the WhatsApp does not reach them, so you can
                show it or keep it but need not send it. Null the moment the
                order is paid or cancelled, because the link is then dead.
            cash_to_collect_cents:
              type:
                - integer
                - 'null'
              description: |
                What the courier collects at the door: the goods value plus the
                customer's share of the fee. Null, not zero, on any order that
                is not cash. Zero is a real value on a cash order whose door
                total is zero (a `0` order value you fully cover); such an order
                is dispatched with no collection to make.
            paid_at:
              type:
                - string
                - 'null'
              format: date-time
          required:
            - method
            - status
            - url
            - cash_to_collect_cents
            - paid_at
        order_value_cents:
          type:
            - integer
            - 'null'
          description: The goods value you sent, untouched.
        fee_cents:
          type: integer
          description: The total delivery fee.
        business_fee_cents:
          type: integer
          description: The part of `fee_cents` charged to you.
        customer_fee_cents:
          type: integer
          description: |
            The part of `fee_cents` charged to the customer. Always sums with
            `business_fee_cents` back to `fee_cents`.
        tip_cents:
          type: integer
          description: |
            The courier tip you sent on this delivery, paid to the courier and
            passed on at cost with no margin taken. Deliberately OUTSIDE
            `fee_cents`, which is the delivery fee `business_fee_cents` and
            `customer_fee_cents` sum back to. 0 when you sent none, and never
            null.
        currency:
          type: string
          enum:
            - MXN
        quote_id:
          type:
            - string
            - 'null'
          description: |
            The `qt_` quote this order bound, so you can tie the price you were
            quoted to the order that spent it. Null when the order was priced
            inline instead.
        pickup:
          type: object
          description: |
            Where the courier collects, snapshotted at create, so a historical
            order keeps reporting where the courier actually went even after the
            store is renamed or moved.
          properties:
            name:
              type: string
            street:
              type: string
            lat:
              type:
                - number
                - 'null'
              format: double
            lng:
              type:
                - number
                - 'null'
              format: double
            notes:
              type:
                - string
                - 'null'
              description: The store's standing pickup instructions, not a per-order value.
          required:
            - name
            - street
            - lat
            - lng
            - notes
        dropoff:
          type: object
          description: Where the courier delivers, echoed back as you sent it.
          properties:
            name:
              type: string
            phone:
              type: string
              description: E.164, normalized on the way in.
              example: '+528112345678'
            street:
              type: string
            apt_unit:
              type:
                - string
                - 'null'
            lat:
              type:
                - number
                - 'null'
              format: double
              description: |
                Null when the address was never geocoded and the text was judged
                instead. Never a fabricated zero.
            lng:
              type:
                - number
                - 'null'
              format: double
            notes:
              type:
                - string
                - 'null'
            handoff:
              type: string
              enum:
                - meet_at_door
                - leave_at_door
                - meet_outside
          required:
            - name
            - phone
            - street
            - apt_unit
            - lat
            - lng
            - notes
            - handoff
        manifest:
          type:
            - object
            - 'null'
          description: |
            The descriptive item list you sent, for the courier. Money never
            comes from it, with one exception: an item sized `xlarge` makes the
            trip a car. Null when you sent none.
          properties:
            items:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                  quantity:
                    type: integer
                  size:
                    type: string
                    enum:
                      - small
                      - medium
                      - large
                      - xlarge
                    description: |
                      What the item was sent as. `small` when you sent none,
                      and on every item created before sizes existed, which is
                      what those items were sent as.
                required:
                  - name
                  - quantity
                  - size
          required:
            - items
        vehicle:
          type: string
          enum:
            - moto
            - car
          description: |
            What the trip was priced and dispatched as: `car` when you asked for
            one or when an item was sized `xlarge`, `moto` otherwise. A car's
            surcharge is inside `fee_cents`. Always present, never null, and
            fixed for the life of the delivery.

            Not the vehicle that arrived. That is `courier.vehicle_type`, which
            can be a car on a `moto` trip without the fee changing.
          example: moto
        courier:
          type:
            - object
            - 'null'
          description: |
            Null until a courier is assigned, and then FROZEN: a terminal
            delivery keeps reporting who carried it.
          properties:
            name:
              type: string
            vehicle:
              type:
                - string
                - 'null'
              description: |
                What the courier is riding, in English whatever locale the
                request carried ("Motorcycle", "Bicycle"). Derived from
                `vehicle_type`, so it is null exactly when that is: display it,
                and switch on `vehicle_type` instead.
            vehicle_type:
              type:
                - string
                - 'null'
              description: |
                The machine-readable type: `bicycle`, `car`, `van`, `truck`,
                `scooter`, `motorcycle` or `walker`. Null until the courier is
                assigned a vehicle. Treat an unlisted value as unknown rather
                than an error: this list grows.
            plate:
              type:
                - string
                - 'null'
            phone:
              type:
                - object
                - 'null'
              description: |
                A masked number and its IVR PIN, never a personal number. Dial
                `masked` and enter `pin` when the system asks. Null when no
                number has been issued for this courier.
              properties:
                masked:
                  type: string
                pin:
                  type:
                    - string
                    - 'null'
              required:
                - masked
                - pin
            location:
              type:
                - object
                - 'null'
              description: |
                The live position, present only while the courier is actually in
                motion and only once there is a real fix.
              properties:
                lat:
                  type: number
                  format: double
                lng:
                  type: number
                  format: double
                updated_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
              required:
                - lat
                - lng
                - updated_at
            imminent:
              type: boolean
              description: >-
                The courier is about a minute away. Always false once the trip
                ends.
          required:
            - name
            - vehicle
            - vehicle_type
            - plate
            - phone
            - location
            - imminent
        pickup_eta_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            When the courier is expected at the store. Absolute, so you compute
            your own countdown; a countdown computed here is stale before you
            read it.
        dropoff_eta_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            When the courier is expected at the customer. Null until there is
            an estimate, and null again once the drop-off has failed
            (`returning`, `returned`): the courier is then bringing the order
            back to the store, there is no arrival at the customer left to
            describe, and the time it gets back to the store is never published
            here.
        tracking_url:
          type:
            - string
            - 'null'
          format: uri
          description: |
            The branded live tracking page for the customer: the map, the
            courier, the delivery code and what is owed at the door. Null when
            there is no page to point at, so you never relay a dead link.

            There is a page from the moment the order is dispatched, including
            the whole window before a courier is assigned, and for 48 hours
            after the order is delivered, canceled or returned. It is null while
            an order is still held awaiting payment or awaiting a confirmed
            drop-off pin, and null again once that 48 hours has passed.
          example: https://app.listo.delivery/t/abc123
        verification:
          type:
            - object
            - 'null'
          description: |
            Present only when the order carries a dropoff PIN: when you asked
            for one (`verification.dropoff_pincode` on the create), or when
            Listo requires a PIN on every delivery of this store's account, in
            which case it is present even though you did not ask. `pincode` is
            null until the order is actually dispatched, which is when the code
            is minted.
          properties:
            pincode:
              type:
                - string
                - 'null'
          required:
            - pincode
        return_verification:
          type: object
          description: |
            The return code (código de devolución). When an order cannot be
            delivered and the courier brings it back to the store, the courier
            must be given this code at the store to hand the order over; without
            it the return cannot be completed. `pincode` is a 4-digit string
            while the delivery is `returning`, and null in every other state
            (before the return, and once it is `returned`). The object is
            always present, so this field never needs a presence check. It
            arrives in `delivery.status_changed` (the move into `returning`) and
            in `delivery.updated`, which carry the whole object. A new attempt
            (a retry) gets its own code. Show it to the store, and never to the
            customer.
          properties:
            pincode:
              type:
                - string
                - 'null'
              example: '4821'
          required:
            - pincode
        proof:
          type:
            - object
            - 'null'
          description: |
            Proof of delivery. Null unless we hold some, and each field is null
            when that piece is missing; a photo with no signature is normal. The
            URLs are Listo's own signed links to the stored copy and are valid
            for 24 hours from the response (or webhook) that carried them:
            fetch `GET /v1/deliveries/{id}` again for a fresh link, or keep a
            copy. **A URL is null until Listo has its own copy of the image**
            (a few seconds after the courier takes it); the copy's arrival is
            announced by a `delivery.updated` webhook that carries the link.
            It may come from a delivery attempt that failed at the
            door, so it is not by itself proof that the order was handed over:
            `proofs.dropoff.delivered` says which. Prefer `proofs`, which keeps
            each moment of the trip apart.
          properties:
            picture_url:
              type:
                - string
                - 'null'
              format: uri
            signature_image_url:
              type:
                - string
                - 'null'
              format: uri
            signature_name:
              type:
                - string
                - 'null'
            captured_at:
              type:
                - string
                - 'null'
              format: date-time
          required:
            - picture_url
            - signature_image_url
            - signature_name
            - captured_at
        proofs:
          type: object
          description: |
            The evidence the trip left, one entry per moment. Each moment is
            null until it happens (nothing has been captured for it yet), so
            this object is always present with its three keys. A photo or
            signature link is Listo's own signed link to the stored copy, and
            **it is null until Listo holds that copy** (a few seconds after the
            courier takes the photo): the rest of the moment, the signer, the
            time and the distance, is there from the start, and when the copy
            lands a `delivery.updated` webhook carries the link. **Each link is
            valid for 24 hours** from the response (or webhook) that carried
            it: it is a short-lived signed link, never a permanent one. A
            partner that needs the image later fetches
            `GET /v1/deliveries/{id}` again for a fresh link, or keeps a copy.
          properties:
            pickup:
              type:
                - object
                - 'null'
              description: |
                The courier collecting the order at the store. Null until the
                pickup photo exists.
              properties:
                photo_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  description: Photo of the order at the store.
                captured_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: When Listo first saw this photo.
              required:
                - photo_url
                - captured_at
            dropoff:
              type:
                - object
                - 'null'
              description: |
                The courier at the customer's door. Present once the order is
                delivered, or once the attempt left a photo, a signature or a
                signer there, **including an attempt that failed**: read
                `delivered`, not the presence of a photo, to know whether the
                order was handed over.
              properties:
                photo_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  description: Photo taken at the door.
                signature_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  description: Image of the recipient's signature.
                signer_name:
                  type:
                    - string
                    - 'null'
                  description: The name the person who signed typed.
                captured_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: When Listo first saw this evidence.
                delivered:
                  type: boolean
                  description: >
                    True only when the delivery was delivered. False means the

                    evidence is from an attempt that did not hand the order
                    over.
              required:
                - photo_url
                - signature_url
                - signer_name
                - captured_at
                - delivered
            return:
              type:
                - object
                - 'null'
              description: |
                The courier handing the order back at the store after a failed
                delivery. Null unless the order came back with evidence.
              properties:
                photo_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  description: Photo of the order handed back at the store.
                signature_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  description: Image of the signature of whoever received it at the store.
                signer_name:
                  type:
                    - string
                    - 'null'
                  description: The name the person who signed typed.
                captured_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: When Listo first saw this evidence.
                meters_from_store:
                  type:
                    - integer
                    - 'null'
                  description: |
                    How far from the store the courier was when this was
                    captured, in whole metres in a straight line. Null when
                    either position is unknown. Evidence for a person to read,
                    not a verdict.
              required:
                - photo_url
                - signature_url
                - signer_name
                - captured_at
                - meters_from_store
          required:
            - pickup
            - dropoff
            - return
        cancellation:
          type:
            - object
            - 'null'
          description: >-
            Who called the order off, and why. Null on an order that was not
            cancelled.
          properties:
            party:
              type: string
              enum:
                - partner
                - courier
                - platform
              description: |
                `partner` is you. `courier` is the person carrying it.
                `platform` is us, and it also covers a cancellation reported to
                us on the customer's behalf: the end customer is not an actor in
                this API, so this field never names a person you cannot
                correlate.
            reason:
              type:
                - string
                - 'null'
              enum:
                - customer_canceled
                - store_closed
                - item_unavailable
                - wrong_address
                - no_longer_needed
                - other
                - null
              description: |
                The reason you sent on a cancel. Null when the attribution was
                derived rather than given, which is every cancellation you did
                not make.
            description:
              type:
                - string
                - 'null'
          required:
            - party
            - reason
            - description
        failure_reason:
          type:
            - string
            - 'null'
          enum:
            - no_couriers_available
            - address_unreachable
            - customer_unavailable
            - delivery_failed
            - null
          description: |
            Why a dispatch failed, bucketed by whose side the problem is on, so
            you can act on it rather than just retry blindly.
            `no_couriers_available` is the retriable one.
            `delivery_failed` is the honest catch-all. Null unless a failure was
            recorded.
        charges:
          type: array
          description: |
            What a trip that was driven but wasted was billed at: the delivery
            fee for each leg driven, less your own store markup. Empty on
            almost every delivery, and never null.

            **This amount changed on 2026-08-29.** It used to be what the
            wasted trip cost Listo, so expect larger values on the same orders
            from that date onward. The field itself is unchanged.

            Always present on a delivery object, so treat an absent `charges`
            as impossible rather than as "no charges". It is in this schema's
            `required` list for that reason.

            A charge can arrive on the delivery's final
            `delivery.status_changed` or, when the cost settles afterwards, on
            a later `delivery.updated`. You cannot assume which, so read this
            on both: billing the moment a delivery reaches its terminal status
            bills short on every order whose cost landed late.

            The same movement appears on `GET /v1/ledger` as a signed row, so
            it reads negative there while `amount_cents` here is positive.
          items:
            type: object
            properties:
              type:
                type: string
                enum:
                  - cancellation_fee
                  - return_fee
                description: |
                  What the charge was for. `cancellation_fee` is a
                  cancellation late enough that a courier was already working
                  on the order. `return_fee` is a return trip after a failed
                  dropoff, and it bills as ONE entry covering both legs of the
                  journey rather than two.
              amount_cents:
                type: integer
                description: |
                  What you were charged, positive, in Mexican peso cents: the
                  delivery fee for each leg that was driven, less your own
                  store markup.
              charged_at:
                type: string
                format: date-time
                description: |
                  When the charge was recorded, which is not when the delivery
                  ended.
            required:
              - type
              - amount_cents
              - charged_at
        refunds:
          type: array
          description: |
            Every time money went back to the customer on this order, newest
            first. Empty on an order that has never been refunded, and never
            null.

            Always present on a delivery object, so treat an absent `refunds`
            as impossible rather than as "no refunds". It is in this schema's
            `required` list for that reason, exactly as `charges` above is.

            It is a LIST because refunds repeat: a partial refund is followed
            by another refund, each with its own amount, its own reason and its
            own outcome. Every refund we can see is here whoever made it, which
            is what makes the list worth reconciling against. The connected
            account belongs to the merchant of record, which can refund a charge
            on it without going through us at all; those arrive as
            `requested_via: external`, and a list that showed only the refunds
            you asked for would silently disagree with this order's own
            `payment.status`.

            A refund rides `delivery.updated`, never an event of its own.
          items:
            type: object
            properties:
              id:
                type: string
                description: The refund id, `rfn_` prefixed.
                example: rfn_4bQ7wS2xK9mT6vN3cZ8pLdYe
              amount_cents:
                type:
                  - integer
                  - 'null'
                description: |
                  What went back, positive, in Mexican peso cents. NULL AND NOT
                  ZERO while a refund of the remaining balance is still in
                  flight: it carries no amount until the refund is confirmed,
                  and a zero would say a refund moved nothing. Read
                  `refunded_cents` below rather than summing this array, which
                  is exactly why both totals are published.
              reason:
                type:
                  - string
                  - 'null'
                description: |
                  The free text whoever asked for the refund supplied, or null.
                  Your own words when you sent them; it is never an enum.
              status:
                type: string
                enum:
                  - pending
                  - succeeded
                  - failed
                description: |
                  `pending` IS NOT A FAILURE and must not be retried: a
                  direct-charge refund is held while the account's balance is
                  short and applied once funds land. It flips to `succeeded` on
                  its own and publishes that as `delivery.updated`. `failed`
                  means nothing moved.
              requested_via:
                type: string
                enum:
                  - api
                  - console
                  - automatic
                  - external
                description: |
                  Which lane issued it. `api` is your own
                  `POST /v1/deliveries/{id}/refunds`. `console` is the merchant
                  pressing it in the Listo console. `automatic` is one we issued
                  with nobody asking, on a cancel or a return. `external` is one
                  made in the merchant's own payment dashboard, which we learned
                  about afterwards.
              party:
                type: string
                enum:
                  - business
                  - platform
                description: |
                  Whose decision it was. `business` is the merchant, or you
                  acting for your store. `platform` is Listo issuing it because
                  the order ended without reaching the customer.
              created_at:
                type: string
                format: date-time
                description: When the refund was asked for.
              succeeded_at:
                type:
                  - string
                  - 'null'
                format: date-time
                description: |
                  When the money was confirmed on its way. Null on a pending or
                  failed refund.
            required:
              - id
              - amount_cents
              - reason
              - status
              - requested_via
              - party
              - created_at
              - succeeded_at
        refunded_cents:
          type: integer
          description: |
            The sum of the SUCCEEDED refunds above, in Mexican peso cents. 0,
            never null, on an order nothing has gone back on. A `pending` refund
            is not counted, because the money has not moved and may yet be
            refused.

            Published rather than left for you to compute, because the array
            cannot always be summed correctly: a refund of the remaining balance
            carries no `amount_cents` until it is confirmed, so adding the
            list up counts it as zero and reads the whole charge as still
            refundable. We take that case pessimistically instead, as having
            taken the entire remaining balance, because guessing low ends in
            refunding a customer twice.
        refundable_cents:
          type: integer
          description: |
            What a further refund could still return: what the customer paid
            (`order_value_cents` plus `customer_fee_cents`, which IS the charge)
            minus `refunded_cents`, floored at zero. 0, never null.

            0 on any order with no collected card payment behind it, whatever
            its value. Cash never touched our rails, a prepaid order's customer
            is yours and we hold nothing of theirs, and an order still awaiting
            payment was never charged, so there is nothing on any of them a
            refund could return.

            This is the ceiling `POST /v1/deliveries/{id}/refunds` refuses an
            `amount_cents` against, so read it before you call rather than
            discovering the bound from a `refund_amount_too_large`.
        support_reference:
          type:
            - string
            - 'null'
          description: |
            A short handle to quote when you escalate an order to us. Null until
            the order reaches dispatch.
          example: 2080F
        pickup_ready_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            The scheduled pickup time you asked for. Null on a dispatch-now
            order.
        requested_at:
          type:
            - string
            - 'null'
          format: date-time
        courier_assigned_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            When a courier was first observed on the current attempt. Honestly
            an observation rather than an assignment clock, because no
            assignment time is published to us. Null on an attempt that never
            had one.
        picked_up_at:
          type:
            - string
            - 'null'
          format: date-time
        delivered_at:
          type:
            - string
            - 'null'
          format: date-time
        canceled_at:
          type:
            - string
            - 'null'
          format: date-time
        returned_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - external_id
        - origin
        - code
        - status
        - store_id
        - test
        - payment
        - order_value_cents
        - fee_cents
        - business_fee_cents
        - customer_fee_cents
        - tip_cents
        - currency
        - quote_id
        - pickup
        - dropoff
        - manifest
        - vehicle
        - courier
        - pickup_eta_at
        - dropoff_eta_at
        - tracking_url
        - verification
        - return_verification
        - proof
        - proofs
        - cancellation
        - failure_reason
        - support_reference
        - pickup_ready_at
        - requested_at
        - courier_assigned_at
        - picked_up_at
        - delivered_at
        - canceled_at
        - returned_at
        - created_at
        - updated_at
        - charges
        - refunds
        - refunded_cents
        - refundable_cents
    ErrorResponse:
      type: object
      description: The body of every 4xx and 5xx answer on this API.
      properties:
        error:
          $ref: '#/components/schemas/Error'
      required:
        - error
    Manifest:
      type:
        - object
        - 'null'
      description: |
        The descriptive item list for the courier. Money never comes from it;
        its only effect on the trip is that an `xlarge` item makes it a car.
        Absent, null, `{}` and `{"items": null}` all mean no cart.
      properties:
        items:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/ManifestItem'
    Error:
      type: object
      description: |
        What every refusal and every failure looks like. `code` is the only
        field to branch on; `message` is for a human reading a log and may
        change wording or language without notice.
      properties:
        code:
          type: string
          description: |
            A stable, machine-readable name for what went wrong, such as
            `validation_failed`, `not_found` or `insufficient_balance`. Branch
            on this and never on `message`. The set grows additively: handle a
            code you do not know as the generic failure its status code says it
            is. Every code has a page under `doc_url`.
          example: validation_failed
        message:
          type: string
          description: |
            A sentence for the person debugging. Free to change wording or
            language, so never match on it.
          example: order_value_cents is required.
        param:
          type:
            - string
            - 'null'
          description: |
            The request field the refusal is about, when one applies, spelled
            as you sent it (`dropoff.street`, `manifest.items`). Null when the
            failure is not about one field.
          example: order_value_cents
        retryable:
          type: boolean
          description: |
            Whether sending the identical request again could succeed. `false`
            means it never will without a change on your side or in the world.
          example: false
        doc_url:
          type: string
          format: uri
          description: The documentation page for this `code`.
          example: https://docs.listo.delivery/en/errors/validation_failed
        request_id:
          type: string
          description: |
            Our id for this request, the same value as the `X-Request-Id`
            response header. Quote it to support, and filter
            `GET /v1/request_logs` by it.
          example: 0b1f6c9a-7e2d-4c53-9a41-5d8e3f2a6b10
      required:
        - code
        - message
        - param
        - retryable
        - doc_url
        - request_id
    ManifestItem:
      type: object
      description: One line of the descriptive item list sent to the courier.
      properties:
        name:
          type: string
          description: What the item is.
        quantity:
          type: integer
          minimum: 1
          description: How many of it. A positive whole number.
        size:
          type:
            - string
            - 'null'
          enum:
            - small
            - medium
            - large
            - xlarge
            - null
          description: |
            How big the item is. Leave it out for `small`. An `xlarge` item
            makes the trip a car.
      required:
        - name
        - quantity
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: >-
        Your API key, sent as `Authorization: Bearer <key>`. Live and test keys
        are separate; the key you send decides which world the call acts in. To
        try calls, use a test key (it starts with `lst_test_`): no real courier
        is sent and no real money moves, but webhooks fire and the
        `dropoff.phone` you give can receive WhatsApp messages, so use your own
        number. A live key (it starts with `lst_live_`) books and charges real
        deliveries.

````

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