Saltar al contenido principal
Cuando algo no funciona, la pregunta es siempre una de tres. ¿Qué envió mi llamada y qué respondió Listo? ¿Listo emitió el evento que yo esperaba? ¿Llegó a mi endpoint, y qué contestó mi endpoint? Cada una tiene un registro que puedes leer tú mismo, por la API o en el panel de socios, sin que nadie en Listo tenga que intervenir. Todo pertenece al modo de la llave que uses: una llave de prueba lee registros de prueba, una llave de producción lee los de producción, y ninguna ve los de la otra. Los ids son los mismos que usa el resto de la API y son opacos: nunca interpretes uno. Toda lista tiene la misma forma que el resto de la API: un arreglo data, un booleano has_more y un cursor opaco, de más reciente a más antiguo, hasta 50 filas por página.

Registro de solicitudes

Cada llamada que tus llaves hacen a /v1/ queda registrada: el método y la ruta, el estatus con que respondimos, el error.code cuando falló, cuánto tardó, qué llave la hizo (sus primeros caracteres, nunca el secreto) y si fue una llamada de prueba. Cada fila trae un request_id, el valor del encabezado X-Request-Id de la respuesta que recibiste, así que es lo que debes citar si alguna vez le escribes a soporte. Ese id es de Listo: toda respuesta lo trae en X-Request-Id, y todo error lo repite en el cuerpo como error.request_id, junto a un doc_url que lleva a la página que explica el error. Si mandas tu propio X-Request-Id, regresa en la respuesta (Listo limpia los caracteres que no acepta) y es el id que se guarda, así que puedes cruzarlo con tus propios registros. El id de un balanceador u otro servicio delante del nuestro no es este. Para encontrar una llamada a partir de un id que copiaste de un error o de un registro, filtra la lista con request_id (coincidencia exacta) o pégalo en el buscador de la página Registro de solicitudes del panel. Una llamada a una herramienta del servidor MCP escribe las mismas filas, y cada una trae el X-Request-Id de la respuesta de /mcp que recibiste. Una herramienta de dos pasos son dos peticiones a /mcp con dos ids. Una vista previa que llama a la API para leer tus datos escribe una fila GET con el id de la vista previa; una que no llama a ningún endpoint no escribe fila. La confirmación escribe la fila de la llamada que hizo el trabajo, y confirmar el mismo token otra vez devuelve el primer resultado y no escribe fila.
La lista se acota con status (failed es toda respuesta de 400 en adelante, succeeded es el resto), endpoint (el valor exacto que muestra una fila, como api/v1/deliveries#create), request_id (una sola llamada, por su id exacto), y created_after y created_before, ambos instantes ISO-8601. Un status que no sea ninguno de los dos valores no coincide con nada, y una fecha que no se pueda leer es un 422 validation_failed que nombra el parámetro. GET /v1/request_logs/{id} agrega los dos cuerpos, request_body y response_body, exactamente como se guardaron. Esa es la respuesta a “por qué se rechazó esto”: el cuerpo de la respuesta es el sobre de error que recibiste, y el cuerpo de la solicitud es lo que realmente enviaste, que muchas veces no es lo que querías enviar. Qué se registra y qué no:
  • Las llamadas que identificaron una llave. Una llamada sin llave, o con una equivocada o revocada, no pertenece a nadie y no está en tu registro, y tampoco una llamada rechazada por límite de frecuencia. Si estás seguro de haber hecho una llamada y no aparece, la razón es una de esas.
  • Las llaves y los enlaces de un solo uso se enmascaran. Una llave de API en cualquier cuerpo aparece como sus primeros caracteres seguidos de [key masked], y el enlace de la respuesta de POST /v1/connect_requests o de una llamada de onboarding de Stripe aparece como [redacted: one-time link], porque tener ese enlace es la autoridad para usarlo. Lo que tú recibiste no se toca; solo se enmascara la copia guardada. Los encabezados nunca se guardan.
  • Un cuerpo se corta a 64 KB. Uno más largo se guarda como un objeto con _listo_truncated: true, los original_bytes que tenía y los primeros 64 KB de su text, así que siempre se te avisa que no estás leyendo todo.
  • Las tres lecturas de depuración no guardan cuerpos. Leer el registro de solicitudes, los eventos o las entregas de webhook también queda registrado, pero sin cuerpos: si no, el registro se copiaría a sí mismo cada vez que lo lees. Su request_body y response_body son null.
  • Las filas se conservan. No hay ventana de retención contra la que correr. Una fila de antes de que existiera este registro no tiene path ni request_id, ambos null.
Los cuerpos llevan datos de tus clientesLos cuerpos de solicitud y respuesta de un envío contienen el nombre, el teléfono y la dirección que enviaste. Los ven las personas de tu equipo que pueden entrar al panel. Trata lo que copies de ahí como tratas los pedidos mismos.

Eventos

GET /v1/events lista lo que Listo emitió para ti, lo haya recibido o no tu endpoint, y GET /v1/events/{id} lee uno. Cada elemento es exactamente el sobre de webhook que recibe tu endpoint (id, type, created_at, data), así que puedes comparar lo que guardaste con lo que quisimos decirte. Se acota con type (un nombre publicado, como delivery.status_changed), delivery_id (los eventos de un envío; un id que no es tuyo da 404), y created_after y created_before. delivery.courier_location queda fuera a menos que lo pidas con type. Se emite aproximadamente cada veinte segundos por cada viaje en camino y nunca se vuelve a enviar, así que enterraría todo lo demás en la página.

Entregas de webhook

Un evento es algo que Listo decidió contarte. Una entrega de webhook es un intento de contártelo: la URL a la que hicimos POST, el estatus con que contestó tu endpoint, los primeros 500 caracteres de su cuerpo, cuánto tardó y si lo contamos como exitoso. GET /v1/webhook_deliveries las lista.
status_code es null cuando nada contestó (un tiempo agotado, una conexión rechazada, un error de certificado), y error_message dice cuál. attempt cuenta los intentos del evento desde 1, incluidos los reintentos que Listo hace por su cuenta cuando tu endpoint falla (ve recibe y verifica webhooks). Se acota con event_id (un evento que no es tuyo da 404), status (failed o succeeded) y created_after y created_before. El orden habitual de preguntas es: encuentra el evento en la lista, mira sus intentos, y lee status_code y response_body para ver qué dijo tu endpoint. Un 400 de tu propia verificación de firma, un 5xx, un tiempo agotado y un evento sin ningún intento (no había endpoint configurado para el modo) son cuatro problemas distintos, y la fila del intento los distingue.

Reenviar un evento

POST /v1/events/{id}/resend envía un evento a tu endpoint una vez más, sin cuerpo. Responde 202 con el sobre del evento, y el envío ocurre enseguida: el intento nuevo aparece en GET /v1/webhook_deliveries con resent: true, numerado después del último del evento.
Qué es un reenvío, para que puedas confiar en él:
  • El mismo evento. Tiene el mismo id y un cuerpo idéntico byte por byte, así que un receptor que deduplica por id, como debe, lo trata como el evento que ya vio. Para ejercitar tu manejador otra vez, borra primero ese registro.
  • Firmado ahora. La firma lleva la hora actual y el secreto vigente, así que verifica aunque el evento tenga días.
  • Enviado a la URL actual. Si cambiaste tu endpoint desde que ocurrió el evento, el reenvío va al nuevo.
  • Una sola vez. No hay reintentos detrás. Si tu endpoint falla, ves un intento fallido y tú decides qué sigue.
  • Con límite. Un evento se puede reenviar 5 veces por hora. El sexto se rechaza con 429 resend_limit_reached, que se puede reintentar pasada la hora. Retry-After dice cuántos segundos faltan para que puedas enviar de nuevo.
  • Nunca una posición de repartidor. Un evento delivery.courier_location lo reemplaza el siguiente en segundos, así que pedir reenviar uno es 409 event_not_resendable.
  • Necesita un endpoint. Sin URL de webhook configurada para el modo de la llave, la respuesta es 409 webhook_endpoint_not_configured.
Un reenvío es una instantánea, no el estado actualEl cuerpo es la instantánea del momento en que ocurrió el evento. Reenviar un delivery.status_changed de hace un día entrega un estatus que el pedido dejó atrás hace mucho. Usa un reenvío para depurar tu endpoint o para recuperar un evento que sabes que perdiste. Para ponerte al día después de una caída, vuelve a leer los envíos con GET /v1/deliveries: eso siempre es la verdad actual. Ve cuando te perdiste algunos.

Enviar un webhook de prueba

No necesitas un pedido para ejercitar tu endpoint. POST /v1/test_webhooks envía un evento sintético de cualquier tipo publicado a la URL de webhook del modo de la llave.
Responde 202 con el evento tal como lo recibe tu endpoint, y el intento aparece en GET /v1/webhook_deliveries como con cualquier otro evento:
Cuatro cosas que debes saber de un evento de prueba. La primera te toca a ti: revisa test antes de actuar.
  • "test": true va en el sobre. Un evento real no trae ese campo. Revísalo antes de hacer cualquier cosa con consecuencias.
  • Sus ids no resuelven a nada. data es una muestra realista del tipo, pero el envío, la tienda, el retiro o la cotización que nombra no existe, así que GET /v1/deliveries/{id} con ese id responde 404. Un manejador que vuelve a leer el objeto, como debe, no encontrará nada, y esa es la prueba de que es de prueba.
  • Funciona en los dos modos. Una llave de prueba envía a tu endpoint de prueba y una llave de producción envía al de producción, así que puedes verificar un endpoint de producción sin hacer un pedido. Ese endpoint de producción sí recibe el evento de prueba, así que asegúrate de que ahí se ignore.
  • Tiene límite. 30 eventos de prueba por hora por modo, y luego 429 test_webhook_limit_reached, y Retry-After dice cuántos segundos faltan para que puedas enviar de nuevo. Un type que no es un nombre publicado es 422 validation_failed con param: "type", y un modo sin URL de webhook es 409 webhook_endpoint_not_configured.
Un evento de prueba aparece en GET /v1/events como cualquier otro, con test: true.

Con qué rechazan estos endpoints

En el panel

Los mismos registros están en el panel de socios, para el modo que tengas seleccionado.
  • Registro de solicitudes lista tus llamadas con los mismos filtros, y cada llamada se abre para ver sus cuerpos de solicitud y respuesta.
  • Webhooks guarda la configuración del endpoint y muestra cada intento. Reenviar está en cada intento de un evento que se puede reenviar, y Enviar webhook de prueba manda un evento sintético al endpoint de arriba.
Esos dos botones hacen lo que hacen los endpoints, con los mismos límites.

Depura con tu asistente de IA

Si ya conectaste un asistente de IA, él mismo puede leer los tres registros, así que puedes preguntarle en palabras sencillas: “¿por qué falló mi última reserva?”, “¿Listo envió el evento de entregado de ese pedido?”, “¿qué respondió mi endpoint?”. Usa los mismos registros y los mismos límites que los endpoints de arriba: resend_event y send_test_webhook actúan de inmediato: a diferencia de una reserva o un retiro no mueven dinero, así que el asistente no te pide confirmarlos. Conservan los límites descritos arriba, y el intento que provocan se lee con list_webhook_deliveries. Cuando a un asistente se le indica esperar por un límite, el error trae retry_after_seconds, los segundos que faltan para que pueda volver a intentar.

Una forma de trabajar

  1. Reproduce el problema con una llave de prueba, para que nada real esté en juego.
  2. Abre la llamada en el registro de solicitudes. El estatus, el error.code y el message del cuerpo de la respuesta, y tu propio cuerpo de solicitud, contestan la mayoría de las preguntas.
  3. Si era un evento que esperabas, encuéntralo en GET /v1/events, y luego sus intentos en GET /v1/webhook_deliveries.
  4. Arregla el manejador y luego reenvía ese evento, o envía un webhook de prueba, hasta que el intento diga succeeded.
  5. Comprueba lo mismo en el endpoint de producción con un webhook de prueba antes de depender de él.