Saltar al contenido principal
Un envío cambia por su cuenta: se asigna un repartidor, se mueve un estimado, un cliente paga, un pedido llega. Los webhooks son la forma de enterarte sin preguntar. El sondeo funciona, pero la posición de un repartidor cambia cada veinte segundos y ningún intervalo de sondeo es a la vez barato y actual.

Registra un endpoint

Configura la URL del endpoint y su secreto de firma en el panel de socios. Cada modo tiene su propio par, así que tus eventos de prueba nunca llegan a tu endpoint de producción y ningún despliegue de staging puede enterarse de un pedido real. Listo hará POST de JSON a esa URL con tres encabezados que importan:
No hay encabezado Authorization ni id de cuenta en el cuerpo. La firma es la identificación: es lo que prueba que la petición vino de Listo y no fue alterada en el camino, y es lo único que lo prueba.

El sobre

Cada evento tiene los mismos cuatro campos, sea cual sea su tipo.
Para todos los tipos salvo delivery.courier_location, data es el objeto completo, con la misma forma con la que responden los endpoints REST, así que un evento es directamente comparable con lo que ya guardaste. El data de arriba está recortado; uno real trae todos los campos que tiene un envío. Un evento que pides tú mismo con un webhook de prueba trae un quinto campo, "test": true, y describe objetos que no existen. Los eventos reales nunca lo traen. store.updated es una señal, no una respuesta: el objeto de tienda no trae un campo de disponibilidad de tarjeta, así que cuando te llegue uno, vuelve a leer GET /v1/stores/{id}/stripe_status. store.connected se dispara en cuanto el dueño aprueba, y no espera a su navegador: puede llegarte antes de que el navegador regrese con el código, y te sigue llegando cuando el navegador nunca regresa. Ve conecta un restaurante que ya usa Listo.

Verifica antes de confiar

Revisa la firma antes de parsear el cuerpo, y rechaza con un 400 cualquier cosa que no cuadre. Cualquiera puede hacer POST a tu endpoint; la firma es lo que separa a Listo de los demás.
verify_webhook_signature.rb
Ahí dentro hay tres detalles que sostienen todo, y la página de firmas explica por qué cada uno lo hace: lo que se hashea es el cuerpo en crudo, la marca de tiempo se revisa en ambas direcciones, y la comprobación corre contra una lista de secretos para que una rotación no tire eventos.

Responde y después trabaja

Contesta 2xx en cuanto la firma cuadre y haz el trabajo real después. Listo espera 5 segundos una respuesta y no sigue redirecciones, así que un endpoint lento se lee como una entrega fallida y una redirección se lee como un error. Tres propiedades sobre las que construir:
  • Al menos una vez. El mismo evento puede llegar dos veces. Deduplica por id, que es estable en cada reenvío de ese evento.
  • Sin orden. Dos eventos enviados casi al mismo tiempo pueden llegar en cualquier orden. Ordena por las marcas de tiempo del propio objeto y no por la llegada, y trata un estado por el que ya pasaste como noticia vieja, no como un retroceso.
  • Aditivo. Pueden aparecer tipos de evento nuevos y campos nuevos dentro de v1. Ignora lo que no reconozcas en lugar de fallar con ello.
Cualquier cosa que no sea 2xx se reintenta con huecos crecientes, unos seis intentos en total. Un delivery.courier_location nunca se reintenta: ya viene en camino otra posición, y reenviar una vieja atrasaría tu mapa en lugar de ponerlo al día.

Cuando te perdiste algunos

Si tu endpoint estuvo caído, vuelve a leer los envíos: ellos son el registro, y los eventos solo fueron un aviso sobre ellos. Un evento reenviado no puede hacer esto por ti, porque su cuerpo es la instantánea del momento en que ocurrió el evento, así que un cambio de estatus de hace un día te habla de un estado que el pedido dejó atrás hace mucho. Leer GET /v1/deliveries siempre te da cómo están las cosas ahora. Para ver qué te perdiste, o para enviar un evento otra vez mientras arreglas un manejador, ve depurar tu integración: puedes listar cada evento y cada intento de entregarlo, reenviar un evento y enviar un evento de prueba de cualquier tipo.
reconcile_deliveries.rb
Esa es también la forma de cada listado en esta API: un arreglo data, un booleano has_more y un cursor opaco que regresas para obtener la siguiente página. Las páginas traen hasta 50 filas y vienen de la más reciente a la más antigua. El cursor es una cadena sin significado propio, así que nunca construyas uno ni lo analices. El dinero tiene su propio registro, exactamente igual. GET /v1/ledger lista cada movimiento de tu saldo, así que un periodo se puede conciliar sin ninguno de los eventos que lo anunciaron. Ve saldo y retiros.

Estados a los que vale la pena reaccionar

La mayoría de los pedidos toma la línea de abajo. Un envío sin pickup_ready_at y sin nada que Listo le tenga que cobrar al cliente, que es todo pedido inmediato en efectivo o prepagado, nace en dispatching y nunca pasa por scheduled. scheduled significa una recolección futura y nada más, y un pedido con tarjeta también llega ahí si se paga antes de que se abra su ventana. awaiting_location es un pedido que la tienda tomó por la consola de Listo o por su asistente de WhatsApp y cuyo punto de entrega Listo todavía no da por bueno. Queda retenido, sin pedir repartidor, hasta que el cliente confirma la ubicación (en un enlace con mapa o compartiéndola en el chat de WhatsApp) o la tienda la confirma desde su consola; entonces pasa a scheduled o a dispatching. También puede terminar en no_courier, cuando no se puede pedir un repartidor para esa ubicación (queda fuera de donde reparte la tienda, o la tienda misma no puede pedir un repartidor por ahora), o en canceled. Un pedido con tarjeta llega ahí una vez pagado. Un envío que creas por la API nunca está en este estado, así que solo lo ves en tiendas cuyos dueños usan la consola o el asistente. returning es un envío todavía vivo, no final: la entrega falló y el repartidor está regresando el pedido a la tienda. Termina en returned cuando el pedido ya está de vuelta. El cargo del regreso ya viene en charges cuando empieza, y las posiciones del repartidor siguen llegando. delivered, canceled y returned son finales. A canceled se puede llegar desde cualquier punto anterior. no_courier es el único callejón sin salida del que sí puedes salir: nadie tomó el pedido. Reinténtalo con POST /v1/deliveries/{id}/retries o cancélalo, y lee failure_reason para decidir cuál. Ese campo es uno de cuatro valores, agrupados según de qué lado está el problema: Un envío returned es el mismo envío de principio a fin: el pedido regresó a la tienda en lugar de convertirse en un segundo pedido, y las posiciones del repartidor siguen llegando durante el tramo de regreso. El evento delivery.status_changed a returning trae el código de devolución de la tienda, que tu sistema tiene que mostrarle al personal de la tienda (ve el código de devolución). En una cancelación, cancellation.party dice quién la pidió: partner por tu lado del pedido, courier por la persona que lo lleva, y platform por todo lo demás que Listo se atribuye a sí mismo. cancellation.reason y cancellation.description traen el resto.
Lo que nunca va en un payloadLos eventos de envío llevan el nombre, el teléfono y la dirección de tu cliente, porque tú se los diste a Listo y el repartidor los necesita. Nada más viaja con ellos: ningún dato de tarjeta, ningún identificador interno, y nada que nombre a quien realiza el viaje. Los estados, los errores, los enlaces de rastreo y las referencias de soporte son vocabulario propio de Listo de principio a fin.

El estado final no siempre es el último evento

Un envío que termina en canceled o en returned puede costar dinero después de terminar. Una cancelación tan tarde que ya había un repartidor trabajando, o un viaje de devolución tras una entrega fallida, se te cobra a la tarifa de envío por cada tramo que se manejó, y ese cargo apenas queda resuelto cuando ya se procesó la cancelación o la devolución. Eso normalmente es más tarde que el cambio de estado que lo provocó, a veces por un buen rato. A veces eso se resuelve junto con la cancelación misma, y el delivery.status_changed final ya llega con charges lleno. A veces no, y el cargo se registra después, en un envío cuyo evento de estado final atendiste hace rato. Entonces llega como un delivery.updated:
charges viene en todo envío y es [] hasta que se registra algo, que en casi todo pedido es para siempre. type es cancellation_fee o return_fee, y amount_cents es lo que se te cobró por él: la tarifa de envío por cada tramo que se manejó, menos tu propio margen de tienda. Es un campo del objeto de envío como cualquier otro, así que viene en toda respuesta que traiga uno: GET /v1/deliveries/{id} tiene la lista completa. De cualquiera de las dos formas se te avisa una sola vez: un cargo que ya venía en el evento final no genera además un evento propio. Lo que no puedes es dar por hecho cuál de los dos lo trajo, así que lee charges del evento que te toque. Trata el arreglo como la verdad actual de ese envío y no como una lista de cargos nuevos desde el último evento, y arma tu facturación sobre el id del envío. Los eventos llegan al menos una vez, así que el mismo charges lleno te puede llegar dos veces, y quien vaya sumando lo que trajo cada evento termina cobrando el mismo viaje otra vez. delivery.courier_location es el único evento que nunca trae nada de esto: su cuerpo es el objeto de posición recortado, sin envío adentro.
Si facturas con el evento final, atiende también el tardíoTratar el delivery.status_changed a canceled o returned como el fin del pedido es la forma natural de construir esto, y funciona en todo pedido cuyo costo ya estaba resuelto. Falla en los demás: en ese momento charges todavía está vacío y se llena después, así que una factura armada desde ahí queda corta por todo el cargo. O dejas el pedido abierto para un delivery.updated posterior, o vuelves a leer el envío con GET /v1/deliveries/{id} antes de facturar.Si para entonces ya cerraste tus libros, ciérralos desde los movimientos de tu saldo: GET /v1/ledger trae ese mismo cargo como una fila provider_charge contra ese envío.