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

# Retrieve a webhook event

> One event, as the envelope your endpoint is sent. An event that is not
yours, or belongs to the other mode, is `404 not_found`.




## OpenAPI

````yaml /openapi/en.yaml get /v1/events/{id}
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/events/{id}:
    get:
      tags:
        - Events
      summary: Retrieve a webhook event
      description: |
        One event, as the envelope your endpoint is sent. An event that is not
        yours, or belongs to the other mode, is `404 not_found`.
      operationId: getEvent
      parameters:
        - name: id
          in: path
          required: true
          description: >-
            The event's `evt_` id. An event that is not yours, or belongs to the
            other mode, is `404 not_found`.
          schema:
            type: string
          example: evt_9pQ2vX7mKdR4tYb1nZ3sLwCe
      responses:
        '200':
          description: The request succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEvent'
              example:
                id: evt_9pQ2vX7mKdR4tYb1nZ3sLwCe
                type: delivery.status_changed
                created_at: '2026-08-01T12:00:00Z'
                data:
                  id: dlv_7hK2mQ9xR4tV6bN1cZ8sLwYe
                  status: scheduled
                  code: LS-2001
                  test: false
                  proof: null
                  origin: api
                  pickup:
                    lat: 20.691145
                    lng: -103.426885
                    name: Tortería La Mera Mera
                    notes: null
                    street: Av. Juan Palomar y Arias 765
                  proofs:
                    pickup: null
                    return: null
                    dropoff: null
                  charges: []
                  courier: null
                  dropoff:
                    lat: 20.709395
                    lng: -103.413393
                    name: Ana Torres
                    notes: Ring buzzer 3
                    phone: '+528112345678'
                    street: Blvd. Puerta de Hierro 4965
                    handoff: meet_at_door
                    apt_unit: Apt 3
                  payment:
                    url: null
                    method: cash
                    status: unpaid
                    paid_at: null
                    cash_to_collect_cents: 5410
                  refunds: []
                  vehicle: moto
                  currency: MXN
                  manifest: null
                  quote_id: null
                  store_id: st_4bR8kM2wQ7xT1nV5cZ9pLdYe
                  fee_cents: 1780
                  tip_cents: 0
                  created_at: '2026-11-06T19:00:20Z'
                  updated_at: '2026-11-06T19:00:20Z'
                  canceled_at: null
                  external_id: order-841
                  returned_at: null
                  cancellation: null
                  delivered_at: null
                  picked_up_at: null
                  requested_at: null
                  tracking_url: https://app.listo.delivery/t/abc123
                  verification: null
                  pickup_eta_at: null
                  dropoff_eta_at: null
                  failure_reason: null
                  refunded_cents: 0
                  pickup_ready_at: null
                  refundable_cents: 0
                  order_value_cents: 3630
                  support_reference: null
                  business_fee_cents: 0
                  customer_fee_cents: 1780
                  courier_assigned_at: null
                  return_verification:
                    pincode: null
      security:
        - BearerAuth: []
components:
  schemas:
    WebhookEvent:
      description: |
        Every event your webhook endpoint receives, and every event the
        debugging reads return: one of the six below, told apart by `type`.
        `data` is the object the event is about, and its shape depends on
        `type`. See `WebhookEventEnvelope` for the fields they all share and for
        how delivery behaves (at-least-once, unordered).
      oneOf:
        - $ref: '#/components/schemas/DeliveryStatusChangedEvent'
        - $ref: '#/components/schemas/DeliveryUpdatedEvent'
        - $ref: '#/components/schemas/DeliveryCourierLocationEvent'
        - $ref: '#/components/schemas/WithdrawalUpdatedEvent'
        - $ref: '#/components/schemas/StoreUpdatedEvent'
        - $ref: '#/components/schemas/StoreConnectedEvent'
      discriminator:
        propertyName: type
        mapping:
          delivery.status_changed: '#/components/schemas/DeliveryStatusChangedEvent'
          delivery.updated: '#/components/schemas/DeliveryUpdatedEvent'
          delivery.courier_location: '#/components/schemas/DeliveryCourierLocationEvent'
          withdrawal.updated: '#/components/schemas/WithdrawalUpdatedEvent'
          store.updated: '#/components/schemas/StoreUpdatedEvent'
          store.connected: '#/components/schemas/StoreConnectedEvent'
    DeliveryStatusChangedEvent:
      allOf:
        - $ref: '#/components/schemas/WebhookEventEnvelope'
        - type: object
          properties:
            type:
              type: string
              description: Which event this is.
              enum:
                - delivery.status_changed
            data:
              $ref: '#/components/schemas/Delivery'
          required:
            - type
            - data
    DeliveryUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/WebhookEventEnvelope'
        - type: object
          properties:
            type:
              type: string
              description: Which event this is.
              enum:
                - delivery.updated
            data:
              $ref: '#/components/schemas/Delivery'
          required:
            - type
            - data
    DeliveryCourierLocationEvent:
      allOf:
        - $ref: '#/components/schemas/WebhookEventEnvelope'
        - type: object
          properties:
            type:
              type: string
              description: Which event this is.
              enum:
                - delivery.courier_location
            data:
              $ref: '#/components/schemas/CourierPing'
          required:
            - type
            - data
    WithdrawalUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/WebhookEventEnvelope'
        - type: object
          properties:
            type:
              type: string
              description: Which event this is.
              enum:
                - withdrawal.updated
            data:
              $ref: '#/components/schemas/Withdrawal'
          required:
            - type
            - data
    StoreUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/WebhookEventEnvelope'
        - type: object
          properties:
            type:
              type: string
              description: Which event this is.
              enum:
                - store.updated
            data:
              $ref: '#/components/schemas/Store'
          required:
            - type
            - data
    StoreConnectedEvent:
      allOf:
        - $ref: '#/components/schemas/WebhookEventEnvelope'
        - type: object
          properties:
            type:
              type: string
              description: Which event this is.
              enum:
                - store.connected
            data:
              $ref: '#/components/schemas/Store'
          required:
            - type
            - data
    WebhookEventEnvelope:
      type: object
      description: |
        The fields every event shares. Delivery is at-least-once and unordered
        by design: dedupe on `id`, and order by `created_at` or by the object's
        own timestamps rather than by arrival. `type` and `data` are added by
        each event, because their values depend on which event it is.
      properties:
        id:
          type: string
          description: The event id, `evt_` prefixed. Dedupe on this.
          example: evt_9pQ2vX7mKdR4tYb1nZ3sLwCe
        created_at:
          type: string
          format: date-time
          description: When the event was recorded, ISO-8601 in UTC.
          example: '2026-07-28T18:41:03Z'
        test:
          type: boolean
          description: |
            Present, and true, only on a test event you asked for with
            `POST /v1/test_webhooks`. A real event does not carry the field at
            all. A test event's `data` is a sample whose ids resolve to nothing.
          example: true
      required:
        - id
        - created_at
    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
    CourierPing:
      type: object
      description: |
        The reduced body of a `delivery.courier_location` event. Correlate it
        with a delivery you already hold; it is not the delivery object.
      properties:
        delivery_id:
          type: string
          description: The `dlv_` id of the delivery the courier is carrying.
        external_id:
          type:
            - string
            - 'null'
          description: Your own reference for that delivery, so you need not store ours.
        location:
          type: object
          properties:
            lat:
              type: number
              format: double
            lng:
              type: number
              format: double
          required:
            - lat
            - lng
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            When this position was last confirmed, which is not the same as when
            the event was sent.
      required:
        - delivery_id
        - external_id
        - location
        - updated_at
    Withdrawal:
      type: object
      description: A request to move money out of your balance to your bank account.
      properties:
        id:
          type: string
          description: The withdrawal id, `wd_` prefixed.
          example: wd_2xN6bK9mR4tQ7vC1sZ8pLwYe
        amount_cents:
          type: integer
          description: |
            What your bank receives. On a `same_day` withdrawal the fee is a
            SECOND debit rather than a deduction, so this figure is what lands
            either way and `fee_cents` is what your balance loses on top of it.
        fee_cents:
          type: integer
          description: |
            What the withdrawal cost, IVA included. Always `0` on a `standard`
            withdrawal. Always present.
        currency:
          type: string
          enum:
            - MXN
        kind:
          type: string
          enum:
            - standard
            - same_day
          description: |
            `standard` is the free path: your released balance only, sent in 1
            to 3 business days. `same_day` costs `fee_cents`, can reach cash
            still on hold, and is sent the same day within the hours in
            `POST /v1/withdrawals`.
        status:
          type: string
          enum:
            - pending
            - paid
            - failed
          description: |
            `pending` until a human settles it. The move to `paid` or `failed`
            is what `withdrawal.updated` announces.
        requested_at:
          type:
            - string
            - 'null'
          format: date-time
        due_at:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            When a `same_day` withdrawal is due to have been sent, stamped when
            you requested it and never recalculated afterwards. Null on every
            `standard` withdrawal: the free path promises a range of business
            days rather than an instant.
        resolved_at:
          type:
            - string
            - 'null'
          format: date-time
          description: Null while still pending.
        clave_de_rastreo:
          type:
            - string
            - 'null'
          nullable: true
          description: |
            The SPEI tracking key for the transfer: the identifier your own bank
            uses to locate the deposit. This is what you reconcile a payout
            against a bank statement with, and it is the same key you see in the
            dashboard for this withdrawal.

            Always present, null until the transfer is sent. It stays null
            forever on a `failed` withdrawal, because no transfer happened.
          example: MBAN01002608120001234567
        test:
          type: boolean
      required:
        - id
        - amount_cents
        - fee_cents
        - currency
        - kind
        - status
        - requested_at
        - due_at
        - resolved_at
        - clave_de_rastreo
        - test
    Store:
      type: object
      description: |
        A pickup location. Card readiness is deliberately NOT a field here: it
        is its own endpoint, with its own outstanding-requirements detail.
      properties:
        id:
          type: string
          description: The store id, `st_` prefixed.
          example: st_4bR8kM2wQ7xT1nV5cZ9pLdYe
        external_id:
          type:
            - string
            - 'null'
          description: |
            Your own reference for this location, if you sent one. Unique per
            key world, and filterable on the list, so you can reconcile against
            your own store book without storing our ids.
          example: pos-store-41
        name:
          type: string
          description: >
            The name you gave the store; for a restaurant that connected an

            account with several branches, the business and the branch, the way

            its own customers read it (`Tortería La Mera Mera, Zapopan`). A

            branch whose own name already begins with the business's name, such

            as `Tortería La Mera Mera Centro`, is published as it is. When

            you send a name, a value that begins with the store's business name

            and a comma is read as that business plus the branch, so sending
            back

            the name you read changes nothing.
        phone:
          type: string
          description: E.164, normalized on the way in.
        street:
          type: string
        apt_unit:
          type:
            - string
            - 'null'
        lat:
          type:
            - number
            - 'null'
          format: double
        lng:
          type:
            - number
            - 'null'
          format: double
        pickup_notes:
          type:
            - string
            - 'null'
          description: Standing instructions for the courier collecting here.
        timezone:
          type: string
          description: |
            The IANA zone the store keeps its hours in, normalized to one
            spelling per zone. Derived from the store's coordinates, and
            re-derived whenever the store's position moves. A zone you send
            yourself is used only when the position cannot answer (a store with
            no coordinates, or a position no zone claims), and one that
            disagrees with the position is neither stored nor treated as an
            error.
          example: America/Mexico_City
        default_business_fee_share:
          type: integer
          minimum: 0
          maximum: 100
          description: |
            The share of the delivery fee this store covers by default, as a
            percentage. The rest is charged to the customer. In force only when
            `default_fee_split_mode` is `percent`; in either amount mode this is
            the percentage the store keeps for when it goes back to one.
        default_fee_split_mode:
          type: string
          enum:
            - percent
            - business_amount
            - customer_amount
          description: |
            Which shape this store's default split has, and therefore which of
            the two fields above and below is in force. `percent` splits the fee
            by `default_business_fee_share`; `business_amount` means the store
            covers up to `default_fee_split_amount_cents` of it and the customer
            pays the rest; `customer_amount` is the mirror, the customer paying
            up to that amount and the store covering the rest.

            Read-only. It is set by the restaurant in its own Listo console, and
            sending `default_business_fee_share` on a create or an update moves
            the store back to `percent`.
        default_fee_split_amount_cents:
          type:
            - integer
            - 'null'
          minimum: 0
          description: |
            The amount the two amount modes divide the fee by, in cents. Null in
            `percent` mode, and never null in either amount mode. Read-only.
        access_tier:
          type: string
          enum:
            - api_only
            - wallet
            - full
          description: |
            How much of Listo the store itself uses. An `api_only` store's
            deliveries spend your balance, and a `wallet` or `full` store's
            spend its own; `billing` says which. Writable on create and update,
            and always `api_only` on a test store. A live store of an account
            whose restaurants pay their own Plan Listo cannot be `api_only`: it
            has no console to buy a plan from, so a create or an update that
            asks for it is refused with `422 validation_failed` and
            `param: "access_tier"`.
        markup_cents:
          type: integer
          minimum: 0
          description: |
            Your own markup on every delivery of this store, in cents. It is
            added to the delivery fee, split with the customer like the rest of
            the fee, and credited to your balance when the delivery is
            delivered. Writable on create and update.
        billing:
          type: string
          enum:
            - partner
            - store
          description: |
            Whose balance pays this store's deliveries, read off `access_tier`
            so you never have to derive it: `partner` is yours, `store` is the
            store's own. Read-only. It is not who pays the store's plan, which
            is `plan.paid_by`.
        test:
          type: boolean
        created_at:
          type: string
          format: date-time
        plan:
          description: >
            The store's Plan Listo, or `null` when the store needs no plan: a

            test store, or a live store created before Plan Listo existed, which

            keeps requesting couriers without one. It is also `null` when the

            store pays its own plan (every store of an account whose stores pay

            their own Plan Listo) and the restaurant has not bought one yet: the

            restaurant buys it in its own console, and there is nothing for you

            to pay or to reactivate. It is also `null` for a store of a partner

            on pricing with no Plan Listo, which needs no plan. When it is an

            object every

            key is present, and the ones that do not apply are `null`. See

            [Plan and
            billing](https://docs.listo.delivery/en/guides/plan-and-billing).
          type:
            - object
            - 'null'
          properties:
            status:
              type: string
              enum:
                - active
                - past_due
                - canceling
                - canceled
                - awaiting_payment
              description: >
                `active` is paid up and renews on `next_charge_on` (null on a

                plan that is passing to the restaurant, see `pending_paid_by`).
                `past_due`

                has an unpaid month, and its deliveries are refused with `402

                plan_required` until the charge goes through. `awaiting_payment`

                is a store whose first month your balance and card could not

                cover: it holds no plan yet, `price_cents` is the price it

                will be charged before IVA (the amount needed to pay is that

                plus IVA), every date is null, its deliveries are refused with

                `402 plan_required`, and Listo charges it by itself as soon as

                your balance or card covers it. `canceling` was

                canceled and is served up to the day before `ends_on`.

                `canceled` has ended, and can be restarted.
            price_cents:
              type: integer
              description: |
                What one month of this store's plan costs you, before IVA: your
                price, even while a restaurant that connected to you is still
                paying its own plan. When `paid_by` is `store` because the
                store pays its own plan, it is the price of one `interval` of
                the restaurant's own plan, which is not yours to pay.
              example: 199000
            currency:
              type: string
              example: MXN
            interval:
              type: string
              description: |
                How long one period of this plan is, and the cycle `price_cents`
                is the price of. `month` for every plan you pay. A store that
                pays its own plan reports the interval its restaurant bought,
                so `year` is a restaurant that paid twelve months at once.
              example: month
            current_period_ends_on:
              type:
                - string
                - 'null'
              format: date
              description: |
                The last day of the current period, already paid unless
                `status` is `past_due`.
            next_charge_on:
              type:
                - string
                - 'null'
              format: date
              description: |
                The day you are charged next: the next renewal, or, while a
                restaurant that connected to you still pays its own plan, your
                first charge. Null once the plan is ending or has ended, and
                null while it is `past_due`: the month owed is charged the
                moment your balance or saved card allows, and retried every
                day until it is paid. Null while `courtesy` is true, too. Null
                as well on a plan you pay that is passing to the restaurant
                (`pending_paid_by` is `store`): you pay the period you are in
                and nothing after it. When `paid_by` is `store` because the
                store pays its own plan, it is the day the restaurant's own plan
                renews, not a charge to you.
            canceled_at:
              type:
                - string
                - 'null'
              format: date-time
              description: When you canceled the plan.
            ends_on:
              type:
                - string
                - 'null'
              format: date
              description: >
                The day the plan stops serving the store, which is the day after

                the last day paid, or the day a plan whose charge was pending
                was

                canceled. Null unless the plan is ending or has ended.
            paid_by:
              type: string
              enum:
                - partner
                - store
              description: |
                Who pays this plan: `partner`, which is you, or `store`, the
                restaurant itself. `store` is a restaurant that connected to you
                and still pays its own plan, and every store that pays its own
                Plan Listo because your account is set up that way; none of
                those is yours to pay.
            pending_paid_by:
              type:
                - string
                - 'null'
              description: >
                Who will pay it from `pending_paid_by_on`, when that is about to

                change, or null. `partner` is you: a restaurant that connected
                to

                you and still pays its own plan, until the renewal you pay from.

                `store` is the restaurant: a plan you pay that passes to the

                restaurant at its next renewal because your account is set up so

                that each restaurant pays its own plan. You pay the period you

                are in and nothing after it, so `next_charge_on` is null.
            pending_paid_by_on:
              type:
                - string
                - 'null'
              format: date
              description: |
                The date that change lands (the day the new payer's first
                period starts), or null.
            courtesy:
              type: boolean
              description: |
                `courtesy` is true while Listo waives this store's plan: nothing
                is charged and `next_charge_on` is null.
          required:
            - status
            - price_cents
            - currency
            - interval
            - current_period_ends_on
            - next_charge_on
            - canceled_at
            - ends_on
            - paid_by
            - pending_paid_by
            - pending_paid_by_on
            - courtesy
      required:
        - id
        - external_id
        - name
        - phone
        - street
        - apt_unit
        - lat
        - lng
        - pickup_notes
        - timezone
        - default_business_fee_share
        - default_fee_split_mode
        - default_fee_split_amount_cents
        - access_tier
        - markup_cents
        - billing
        - test
        - created_at
        - plan
  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.