POST /v1/quotes pregunta cuánto cuesta un
envío a una dirección dada y aparta ese precio para ti; POST /v1/deliveries
regresa la cotización y le cobra al pedido el precio que te mostramos.
La cotización es opcional. Tómala cuando una persona vaya a ver el número y
decidir algo, y omítela cuando no haya nadie: una creación sin quote_id se
cotiza sola, en línea, al precio del momento y en una sola llamada.
quote_and_create.rb
La cotización
Una cotización fija el precio en el momento en que la pides y lo guarda para que lo uses. No es un apartado de repartidor. No se despacha nada, no se mueve dinero, y dejar que una expire no te cuesta nada. Si usas una cotización, al pedido se le cobra elfee_cents y el reparto que te
mostramos. La red de repartidores reserva el viaje cuando se crea el envío, y el
precio que reserva puede diferir un poco del cotizado. Reservamos la misma
cotización que viste siempre que siga vigente, así que esto es poco común. Cuando
pasa, la diferencia aparece en tu historial como su propia línea
requote_adjustment (negativa si el viaje reservado costó más, positiva si costó
menos). Nunca cambia el fee_cents, el reparto ni el total que ve tu cliente.
Envías la tienda, el punto de entrega y lo suficiente del pedido para poder
cotizarlo: el método de pago y el valor del pedido cambian los dos lo que cuesta
un envío, así que una cotización tomada sin ellos es la cotización de otro
pedido. Responde con:
expires_at está a lo mucho a quince minutos y con frecuencia a menos. De ahí se
siguen dos reglas, y las dos son rechazos, no sorpresas:
- Crear contra una cotización expirada devuelve
400 quote_expired. Pide una nueva e intenta de nuevo; no se creó nada. - Una cotización es de un solo uso. Gastarla dos veces devuelve
400 quote_consumed, que es como se ve desde aquí un doble clic en el formulario.
La creación
Obligatorio en cada creación:store_id, external_id, payment_method,
order_value_cents, y un dropoff con al menos name, phone y street. Todo
lo demás tiene un valor por defecto.
order_value_cents es el valor de la comida o de la mercancía, y te toca a ti
declararlo. Nunca se suma a partir del manifiesto, y nunca incluye la tarifa de
envío.
Idempotency-Key es obligatorio
Envía un encabezado Idempotency-Key, de máximo 255 caracteres, en cada
creación. No es opcional, y una creación sin él se rechaza.
Derívalo de tu pedido, no del intento. Eso es lo que hace que valga la pena el
encabezado: cuando una creación se va a timeout no sabes si se despachó un
repartidor, y repetirla con la misma llave responde con el envío original en vez
de mandar un segundo. La respuesta original se repite durante 24 horas, código de
estado incluido.
Reutilizar una llave con un cuerpo distinto devuelve
409 idempotency_conflict. Es a propósito: significa que se mandaron dos
pedidos distintos bajo una misma llave, y responder con cualquiera de los dos
estaría mal.
Una repetición que llega mientras la primera solicitud con esa llave todavía se
está procesando también devuelve 409 idempotency_conflict. No creó nada, y no
significa que la primera haya fallado: esa solicitud sigue hasta su propia
respuesta, y en cuanto responde, la misma solicitud con la misma llave la repite.
external_id es cómo lo vas a reconocer
external_id es tu propia referencia del pedido. Regresa en cada respuesta y en
cada webhook, se puede consultar con GET /v1/deliveries?external_id=..., y es
único por modo de llave, así que tu sistema de staging y el de producción pueden
usar los dos order-841 sin chocar nunca. Mandar uno que ya usaste en ese modo
se rechaza.
No tienes que guardar el id dlv_... de Listo si prefieres no hacerlo.
Métodos de pago
En
card la respuesta trae payment.url: el enlace con la marca que Listo le
manda a tu cliente por su cuenta, en cuanto se crea el pedido (consulta
lo que Listo le manda a tu cliente). Puedes
mostrarlo en tu pantalla o guardarlo, pero no tienes que mandarlo, y no debes:
tu cliente recibiría el mismo enlace dos veces, de dos remitentes. El envío se
queda ahí hasta que se pague, y después se despacha solo. El enlace desaparece
del objeto en cuanto se paga o se cancela, porque un enlace muerto en un payload
es peor que ninguno, y Listo no manda nada de un enlace que ya no está vigente.
Con prepaid la tarifa completa es tuya: no hay a quién cobrarle una parte de
cliente en un pedido cuyo cliente Listo nunca cobra. Por eso la parte es siempre
100 y business_fee_cap_cents se ignora, y cómo se nota depende del endpoint:
POST /v1/quotesconprepaidrechaza unbusiness_fee_sharedistinto de 100 con422 validation_failednombrandobusiness_fee_share. Envía 100 o déjalo fuera: sin campos de reparto, la cotización se calcula con 100, sin importar el reparto por defecto de tu tienda. Una cotización que no indicapayment_methodse calcula exactamente igual, porque la única entrega que puede enlazarla es unaprepaid: también rechaza una parte distinta de 100, con un mensaje que lo dice y que explica que puedes indicar elpayment_method. Para repartir la tarifa con tu cliente, cotiza comocashocard.POST /v1/deliveriesconprepaidy sinquote_idfuerza la parte a 100 y crea el pedido.POST /v1/deliveriesconprepaidy unquote_idnecesita una cotización que se haya pedido conpayment_method: prepaido sinpayment_method, y ambas se calculan siempre con 100. Cualquier otra cotización se rechaza con422 validation_failed, nombrando lo primero que no coincide. Una cotización pedida comocashocardse rechaza nombrandopayment_method. Una cotización cuyo reparto no es 100 sin tope (una guardada antes de esta regla) se rechaza nombrandobusiness_fee_share(obusiness_fee_cap_cents), con un mensaje que dice que pidas una cotización nueva comoprepaid.
Lo que Listo le manda a tu cliente
Listo le escribe por su cuenta al cliente de tus pedidos, desde su propio número de WhatsApp y en español, así que no tienes que construir nada para mantenerlo informado. Salen dos cosas:- El enlace de pago, en los pedidos
card. En cuanto se crea un pedidocardy queda esperando el pago, Listo le manda al cliente un WhatsApp con su nombre, el nombre de tu tienda, lo que debe y el enlacepayment.url, y le dice que el repartidor sale en cuanto pague. Si ese WhatsApp no le llega (el número no tiene WhatsApp, o el mensaje falla), Listo le manda el mismo enlace por SMS, desde un código corto, para que lo reciba de todos modos. Se manda una vez por pedido, sin que tengas que llamar nada. Un enlace que ya no está vigente, porque el pedido se pagó o se canceló, no se manda. - Los mensajes del repartidor, en todos los pedidos. Cuando se asigna un
repartidor, cuando va en camino, cuando está llegando y cuando se entrega el
pedido, Listo le manda al cliente un WhatsApp desde el mismo número, con el
enlace de seguimiento y, si lo pediste, el código de entrega. Esto incluye los
pedidos
prepaidycash: haber cobrado en tu propio checkout no impide que Listo le diga al cliente dónde va su pedido.
Propina para el repartidor
tip_cents agrega una propina para el repartidor, en centavos, encima de todo lo
demás del pedido. Le llega completa al repartidor: Listo no le suma nada ni se
queda con nada.
Es opcional, su valor por defecto es 0, y tiene un tope de 100000
(MX$1,000). Cualquier cosa arriba de eso, o por debajo de cero, se rechaza con
validation_failed nombrando tip_cents antes de cotizar el pedido, así que una
propina imposible no te cuesta nada.
Una propina no es parte de fee_cents y no se reparte con tu cliente. Tu parte
de la tarifa y la propina salen juntas de tu saldo, como un solo movimiento, al
crear el envío; y si el envío nunca ocurre, también regresan juntas: un pedido
canceled, returned o no_courier te reembolsa la propina junto con tu parte
de la tarifa.
Una propina se decide una sola vez, al crear, y no se puede subir después. No hay
ninguna llamada que le agregue propina a un envío que ya va en camino.
Programación
Si dejas fuerapickup_ready_at, el repartidor se llama de inmediato. Si lo
mandas, el pedido queda programado: el repartidor se agenda ahora, para una
recolección después.
Tiene que estar a por lo menos 10 minutos y a lo mucho a 29 días. Diez minutos es
poco a propósito, porque “la comida sale en quince” es el caso normal para una
máquina, no el excepcional.
Un pedido programado se puede mover con PATCH /v1/deliveries/{id} mientras
siga en scheduled, incluso a ahora mismo, lo cual lo despacha. Una vez que
salió de scheduled la ventana queda fija y la llamada devuelve
409 not_reschedulable.
Enviarlo en coche
vehicle es moto, el valor por defecto, o car. Manda car para un pedido
que no cabe en una moto.
- Un coche es un sedán. Podemos garantizar un coche, nunca una camioneta.
- Hay menos coches que motos, así que un coche puede tardar más en asignarse.
- Un coche le suma un recargo a la tarifa de envío. Va dentro de
fee_centsy se reparte como el resto de la tarifa, así que el precio que te cotizamos es el precio completo, sin una línea aparte.
manifest.items también puede llevar un size: small (el
valor por defecto), medium, large o xlarge. Un artículo de tamaño xlarge
hace que el viaje sea en coche aunque vehicle diga moto, porque no cabe en
una moto. Ningún otro tamaño lo hace, y tampoco la cantidad de artículos: dos
artículos large siguen siendo un viaje en moto. Manda en la cotización el
mismo vehicle y el mismo manifiesto con los que vas a crear, para que el
precio que muestras sea el del viaje que reservas.
La cotización y el envío responden con vehicle: en qué se cotizó y se despachó
el viaje, sea lo que sea que lo hizo en coche. No es el vehículo que llega. Ese
es courier.vehicle_type en el envío, que de vez en cuando puede ser un coche
en un viaje moto sin que cambie la tarifa.
Un carrito demasiado grande para un solo viaje de repartidor se rechaza con
422 order_too_large, nombrando manifest.items, antes de cotizar o despachar
nada. Divídelo en dos envíos.
El detalle opcional
manifest.itemses una lista de{name, quantity, size}para que el repartidor coteje. Es descriptiva: el dinero sale deorder_value_cents, y lo único que un artículo puede cambiar es el vehículo, con susize(consulta enviarlo en coche).handoffesmeet_at_door(el valor por defecto),leave_at_dooromeet_outside. Los pedidos en efectivo siempre se entregan en persona.dropoff.notesson instrucciones por pedido para esa dirección, de máximo 280 caracteres. Las instrucciones permanentes de la tienda van en la tienda, comopickup_notes.verification.dropoff_pincode: truepide un código que el cliente le tiene que dar al repartidor. Regresa en el envío comoverification.pincodeen cuanto un repartidor tiene uno. Listo también se lo da a tu cliente en sus propios mensajes del repartidor, así que solo necesitas mostrarlo tú si lo quieres en tu pantalla.dropoff.latydropoff.lngson opcionales. Si los dejas fuera, la calle se ubica por ti; si los mandas, se usan tal cual. Manda los dos o ninguno.
Cuando un pedido regresa: el código de devolución
Cuando un envío no se puede entregar, el repartidor regresa el pedido a la tienda y el envío pasa areturning. Para terminar de dejarlo ahí, el
repartidor le pide a la tienda un código de 4 dígitos. Ese código de devolución es
return_verification.pincode en el objeto del envío. El repartidor lo necesita
para terminar la devolución, así que tu sistema tiene que mostrárselo al personal
de la tienda.
return_verificationsiempre viene en el objeto.pincodees una cadena de 4 dígitos mientrasstatusesreturning, ynullen cualquier otro estado, así que muestra el código cuando venga y escóndelo cuando no.- No necesitas sondear. El webhook
delivery.status_changeddel paso areturningtrae el objeto completo del envío, código incluido (ve webhooks). - Es el código de la tienda, nunca el del cliente, y no es
verification.pincode, que es el código de entrega que el cliente le da al repartidor. No se lo mandes a tu cliente. - El personal debe dárselo al repartidor solo cuando ya tenga el pedido en la mano.
- Cada intento tiene su propio código. Un envío reintentado es un intento nuevo, así que recibe un código nuevo.
- Cuando el envío queda en
returned, el código ya se usó ypincodevuelve anull.
Qué recibes de vuelta
Un201 con el objeto de envío completo: la misma forma que vas a leer de
GET /v1/deliveries/{id}, del listado y de cada webhook. Una sola forma en todos
lados, así que un objeto que guardaste al crear es directamente comparable con el
que un evento te entrega después. La lista completa de campos está en
la referencia.
Dos costumbres que vale la pena adoptar desde ahora, porque v1 solo cambia de
forma aditiva: ignora los campos que no reconozcas en lugar de fallar con ellos,
y trata cada id como una cadena opaca que nunca analizas.
Cuándo se rechaza
Cada rechazo usa el mismo sobre. Ramifica segúnerror.code, nunca según el
mensaje, y reintenta solo cuando error.retryable diga que puedes.
Un
out_of_service_area llega después de que algo quedó registrado. La red de
repartidores puede rechazar una entrega justo en la orilla del círculo en la
creación misma, un momento después de cotizar el viaje. La respuesta es el mismo
422, con el mismo cuerpo, y la misma solicitud con la misma Idempotency-Key
la recibe otra vez. Pero para entonces el envío ya existía, así que se queda:
terminado en no_courier con failure_reason: delivery_failed, con tu
external_id, listado por GET /v1/deliveries y anunciado por un webhook
delivery.status_changed. La tarifa que se descontó por él se devolvió completa,
así que tu libro muestra el cargo y su devolución uno junto al otro, y no se
cobró nada más. Reintentarlo solo vuelve a encontrar el mismo rechazo. Para
mandar ese pedido a otra dirección, créalo con un external_id nuevo.
Listo nunca inventa un precio. Un pedido que no puede cotizar se rechaza en lugar
de tomarse a un número inventado, y por eso provider_unavailable es una
respuesta distinta de una falla de validación: una significa intenta otra vez, la
otra significa cambia algo.
no_couriers_available tampoco es una caída. La red de repartidores respondió, y
lo que dijo es que nadie cerca del viaje está libre en este momento, que es el
mismo hecho que un envío terminado sin repartidor reporta como
failure_reason: no_couriers_available. Dale unos minutos, reintenta un número
limitado de veces y después avísale a quien espera el pedido. La cotización lo
responde igual.
Después de despacharlo
GET /v1/deliveries/{id}lee uno, yGET /v1/deliverieslos lista del más reciente al más antiguo, filtrados porstatus,payment_status,store_id,external_id,originy una ventana de creación, y paginados con uncursoropaco.POST /v1/deliveries/{id}/cancellationslo cancela, con un motivo de la lista de Listo:customer_canceled,store_closed,item_unavailable,wrong_address,no_longer_needed, uothercon unadescription. Funciona desde cualquier estado no final, y cancelar un envío ya cancelado simplemente responde con él.POST /v1/deliveries/{id}/retriesvuelve a despachar un envío que terminó enno_courier. El id y tuexternal_idsobreviven al reintento, así que para ti sigue siendo un solo pedido. Si la red de repartidores rechaza la distancia del envío reintentado en la orilla de la zona de servicio, el reintento responde422 out_of_service_area, que es definitivo: el envío se queda enno_couriery no se cobra nada.