Saltar al contenido principal
Despachar un pedido son dos llamadas. 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 el fee_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 tiene que coincidir con la cotizaciónVincular una cotización significa que el precio ya quedó, así que la creación no puede contradecir aquello con lo que se calculó ese precio. payment_method, order_value_cents, business_fee_share, business_fee_cap_cents, la street de entrega y el apt_unit de entrega tienen que coincidir con la cotización, o la creación se rechaza con validation_failed nombrando el campo que no concuerda. Si cambias cualquiera de ellos, necesitas una cotización nueva.El vehículo también tiene que coincidir, y se compara como viaje completo, no solo el campo vehicle: una creación cuyo manifiesto trae un artículo de tamaño xlarge es un viaje en coche diga lo que diga vehicle, así que vincularla a una cotización calculada en moto se rechaza, nombrando manifest.items. Si dejas vehicle fuera, la creación hereda el de la cotización.El mensaje de ese rechazo dice por qué y qué hacer: cuando lo que no concuerda es el reparto, te dice que envíes el mismo reparto o pidas una cotización nueva.Dejar un campo fuera no es contradecir nada: lo que omites se hereda de la cotización. Mandar business_fee_cap_cents al crear contra una cotización que se tomó sin tope no cuenta como agregarlo, sino como contradecirla, así que ponle el tope también a la cotización.tip_cents es la excepción, y la única: las cotizaciones no tienen campo de propina, una propina nunca cambia una tarifa, y cualquier propina que mandes al crear es compatible con cualquier cotización que vincules.

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/quotes con prepaid rechaza un business_fee_share distinto de 100 con 422 validation_failed nombrando business_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 indica payment_method se calcula exactamente igual, porque la única entrega que puede enlazarla es una prepaid: también rechaza una parte distinta de 100, con un mensaje que lo dice y que explica que puedes indicar el payment_method. Para repartir la tarifa con tu cliente, cotiza como cash o card.
  • POST /v1/deliveries con prepaid y sin quote_id fuerza la parte a 100 y crea el pedido.
  • POST /v1/deliveries con prepaid y un quote_id necesita una cotización que se haya pedido con payment_method: prepaid o sin payment_method, y ambas se calculan siempre con 100. Cualquier otra cotización se rechaza con 422 validation_failed, nombrando lo primero que no coincide. Una cotización pedida como cash o card se rechaza nombrando payment_method. Una cotización cuyo reparto no es 100 sin tope (una guardada antes de esta regla) se rechaza nombrando business_fee_share (o business_fee_cap_cents), con un mensaje que dice que pidas una cotización nueva como prepaid.
Ve el modelo de dinero.

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 pedido card y queda esperando el pago, Listo le manda al cliente un WhatsApp con su nombre, el nombre de tu tienda, lo que debe y el enlace payment.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 prepaid y cash: haber cobrado en tu propio checkout no impide que Listo le diga al cliente dónde va su pedido.
No mandes nada tuyo para ninguna de las dos cosas, o el cliente lo recibe dos veces. Todo lo demás que le digas, tu confirmación del pedido o tu recibo, sigue siendo tuyo. Nada de tu saldo, del reparto de la tarifa ni de lo que te cuesta un envío aparece en los mensajes de Listo.

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.
Las propinas son solo para pedidos prepagadostip_cents se acepta con payment_method: prepaid. Una propina mayor a cero en un pedido cash o card se rechaza con validation_failed nombrando tip_cents.Esos dos pedidos son en los que Listo cobra el dinero, y ahí una propina declarada tendría la forma equivocada en ambos casos. En un pedido en efectivo al repartidor se le da la propina en mano en la puerta, por la misma persona que ya le está entregando dinero, y una propina declarada cambiaría lo que el repartidor tiene que cobrar y contaría para el límite de efectivo. En un pedido con tarjeta tu cliente paga con un enlace de Listo, así que una propina que tú fondeas encima de un cargo que él pagó es una segunda propina, distinta. El prepago es el único método en el que ya arreglaste cuentas con tu cliente por tu lado, así que la propina es tuya sin ambigüedad.
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 fuera pickup_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_cents y se reparte como el resto de la tarifa, así que el precio que te cotizamos es el precio completo, sin una línea aparte.
Cada artículo de 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.items es una lista de {name, quantity, size} para que el repartidor coteje. Es descriptiva: el dinero sale de order_value_cents, y lo único que un artículo puede cambiar es el vehículo, con su size (consulta enviarlo en coche).
  • handoff es meet_at_door (el valor por defecto), leave_at_door o meet_outside. Los pedidos en efectivo siempre se entregan en persona.
  • dropoff.notes son instrucciones por pedido para esa dirección, de máximo 280 caracteres. Las instrucciones permanentes de la tienda van en la tienda, como pickup_notes.
  • verification.dropoff_pincode: true pide un código que el cliente le tiene que dar al repartidor. Regresa en el envío como verification.pincode en 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.lat y dropoff.lng son 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 a returning. 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_verification siempre viene en el objeto. pincode es una cadena de 4 dígitos mientras status es returning, y null en cualquier otro estado, así que muestra el código cuando venga y escóndelo cuando no.
  • No necesitas sondear. El webhook delivery.status_changed del paso a returning trae 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ó y pincode vuelve a null.
Listo muestra el mismo código en tu panel de partner, en la fila del envío, mientras el pedido regresa y después de que regresó. El objeto del envío sigue siendo de donde lee el código tu propio sistema. El campo está descrito en la referencia.

Qué recibes de vuelta

Un 201 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ún error.code, nunca según el mensaje, y reintenta solo cuando error.retryable diga que puedes.
Los que se encuentra una creación: 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, y GET /v1/deliveries los lista del más reciente al más antiguo, filtrados por status, payment_status, store_id, external_id, origin y una ventana de creación, y paginados con un cursor opaco.
  • POST /v1/deliveries/{id}/cancellations lo cancela, con un motivo de la lista de Listo: customer_canceled, store_closed, item_unavailable, wrong_address, no_longer_needed, u other con una description. Funciona desde cualquier estado no final, y cancelar un envío ya cancelado simplemente responde con él.
  • POST /v1/deliveries/{id}/retries vuelve a despachar un envío que terminó en no_courier. El id y tu external_id sobreviven 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 responde 422 out_of_service_area, que es definitivo: el envío se queda en no_courier y no se cobra nada.
Dónde queda el dinero cuando hay una cancelación es el modelo de dinero; cómo enterarte de todo esto sin sondear es webhooks.