Saltar al contenido principal
Tu cuenta tiene un saldo por modo. De ahí se pagan los envíos, ahí entra el efectivo que cobran tus repartidores, y los retiros mueven lo que queda a tu banco. Esta página es la mitad operativa; lo que significa cada movimiento está en el modelo de dinero.

Dos números

balance_cents es todo lo que tiene la cuenta. available_cents es la parte que puede salir hoy. Hay dos motivos por los que se retiene dinero fuera de available_cents, y en los dos casos ese dinero se puede gastar en envíos mientras espera:
  • El efectivo que cobraron tus repartidores. Cuando se entrega un pedido en efectivo, el valor del pedido se abona a tu saldo de inmediato. Se vuelve retirable una vez que ese efectivo ya se le liquidó a Listo, en un ciclo semanal.
  • El dinero que metiste con tarjeta. Una recarga con tarjeta se retiene 180 días desde el cargo, que es la ventana en la que un pago con tarjeta todavía se puede disputar. Una transferencia SPEI no tiene esa ventana y es retirable en cuanto cae.
Es decir: crear envíos gasta contra balance_cents; retirar gasta contra available_cents. Una parte pequeña del efectivo es la excepción de la primera mitad. Cada respuesta trae también in_review_cents. Cuando surge una duda sobre un pedido en efectivo mientras liquidamos la semana, el efectivo de ese pedido se queda retenido mientras lo revisamos, y este es el monto de balance_cents que eso representa. Está dentro del efectivo retenido de arriba, así que nunca entra en available_cents y no se puede adelantar, y a diferencia del resto del efectivo retenido no se puede gastar en envíos: crear un envío responde 402 insufficient_balance cuando balance_cents menos in_review_cents no alcanza. Es 0 en casi todas las cuentas, no trae fecha y se libera cuando tenemos una respuesta. Vale la pena planearlo: una cuenta fondeada solo con tarjeta se puede gastar completa y no se puede retirar nada hasta que venza la primera retención, seis meses después. Si piensas retirar, fondea por SPEI.
balance_and_withdrawal.rb

Todos los movimientos, en orden

GET /v1/ledger es el historial detrás de esos dos números: una fila por movimiento de la cuenta, del más reciente al más viejo, en los mismos centavos con signo. Los abonos son positivos y los cargos negativos, así que las filas se suman sin tener que deducir la dirección a partir del tipo.
Esas dos filas son justo el par que describe el modelo de dinero: un envío devuelto regresó completa la parte del negocio y cobró los dos tramos que se manejaron, como dos movimientos y no como un reembolso encogido. Cada fila dice de dónde salió: Esta lista crece. Aparecen valores nuevos de type en cualquier versión, porque los cambios a v1 son aditivos y un movimiento que todavía no nombramos sigue siendo dinero que se movió. Haz tu lógica sobre los valores que conoces y deja pasar el resto: cuenta un type desconocido dentro del saldo, muéstralo, y no lo descartes ni falles con él. Las filas que tires son justo la diferencia entre tus libros y GET /v1/balance.
Los dos campos type son vocabularios distintosEl charges[].type de un envío dice de qué fue el cargo: cancellation_fee o return_fee. El type de una fila del historial dice de dónde salió el movimiento, y todos esos cargos llegan bajo un solo valor: provider_charge.Es el mismo dinero visto desde dos lados. El envío te dice qué se te cobró por ese pedido; el historial te dice hacia dónde se movió en la cuenta, que es por lo que el monto es positivo en uno y negativo en el otro. No mapees un vocabulario sobre el otro, y no esperes ver return_fee como type de una fila.Empata los dos por delivery_id y nunca por id. El id lo tiene solo la fila del historial; el cargo del envío no tiene, porque su manija es el envío mismo.
Una fila que viene de uno de tus pedidos trae su delivery_id, que es como se le atribuye un costo a un pedido. Eso incluye los pedidos que tus tiendas despacharon desde su propia consola de Listo: también son pedidos tuyos y traen el mismo id dlv_, sin external_id porque tú nunca lo enviaste. null significa que el movimiento no pertenece a ningún pedido: una recarga, un retiro, el plan de una tienda o un ajuste. test dice a qué mundo pertenece la fila, aunque una llave solo ve el suyo. store_id dice a qué tienda pertenece un movimiento, y viene en cada fila: la tienda del pedido en una fila que viene de uno, la tienda cuyo plan se pagó en un plan_charge. Viene en null cuando el movimiento no pertenece a una sola tienda, como una recarga o un retiro de tu propio saldo. Dos filtros, y responden las dos preguntas que vale la pena hacer:
  • GET /v1/ledger?delivery_id=dlv_... es todo lo que se movió por un solo pedido. Esto es lo que le facturas a tu propio comercio.
  • GET /v1/ledger?type=provider_charge es cada cargo por viaje desperdiciado que se te ha cobrado, de todos los pedidos.
Los dos prefieren rechazar antes que confundirte. Un type que no reconocemos no empata con nada y devuelve una página vacía, en vez de devolverte calladamente todo, y un delivery_id que no es un pedido tuyo en este modo devuelve 404 not_found y no una página vacía, porque una página vacía es una respuesta verdadera a “ese pedido no costó nada extra” y se leería como tal. Cada uno recibe exactamente un valor. No hay lista separada por comas ni llave repetida, y mandar cualquiera de las dos no amplía el filtro: es un valor que no empata con nada, así que type regresa vacío y delivery_id se rechaza como cualquier otro id que no sea tuyo. Se pagina como cualquier otro listado de aquí: hasta 50 filas, un booleano has_more y un cursor opaco que regresas para pedir la siguiente página. El cursor es null en la última página, así que nunca puede contradecir a has_more sobre si vale la pena pedir otra. Solo camina hacia atrás, hacia el historial. Para agarrar los movimientos que se registraron desde tu último recorrido, vuelve a leer la primera página en lugar de reenviar el cursor con el que terminaste.

Cómo cobrarle un costo al pedido que lo causó

Esta es la conciliación para la que existe el historial: recorres cada cargo por viaje desperdiciado y le cobras cada uno al pedido que lo provocó.
reconcile_charges.rb
Ahí hay tres cosas que conviene copiar en lugar de reescribir. El ciclo sigue regresando el cursor hasta que has_more sea falso, porque un recorrido que lee una sola página nada más le cobra a tus pedidos más recientes. Los montos se suman tal como llegan, porque un cargo es negativo y un valor absoluto convertiría cada cargo en un abono. Y una fila cuyo delivery_id es null se señala en vez de atribuirse, porque adivinar a qué pedido pertenecía es la forma de cobrarle a un comercio el viaje de alguien más.
Concilia con el historial, no con tus eventosLos webhooks son un aviso de que algo pasó. El historial es el registro de lo que pasó, así que un evento perdido, uno reenviado o un endpoint caído una hora no cambian nada de lo que puedes rearmar después. Recorre el historial del periodo que estás cerrando y ahí está cada movimiento, en orden y una sola vez.

El saldo de una sola tienda

Todo lo de arriba es tu cuenta. Una tienda puede ser dueña de su dinero, que es lo que decide el access_tier del objeto de tienda. En el modelo de dinero está qué significa ese arreglo; aquí está cómo leerlo. Dos endpoints responden por una tienda, y responden por todas las tiendas y no solo por las que son dueñas de su dinero:
Los primeros tres campos significan exactamente lo mismo que en GET /v1/balance. El cuarto dice de quién es el saldo que estás viendo: "store" es el de la tienda, "partner" es el tuyo, que es lo que reporta una tienda en el tier por defecto porque ese sí es el saldo del que gastan sus envíos. Dos tiendas que gastan de tu saldo reportan la misma cifra, y es la misma que da GET /v1/balance. El historial es la misma ventana con la misma forma: los mismos campos por fila, el mismo vocabulario de type, las mismas páginas de 50 filas y el mismo cursor opaco, y los dos filtros funcionan igual. Lo que hayas escrito para recorrer GET /v1/ledger recorre este sin cambiarle nada.
Una tienda que gasta de tu saldo muestra tus movimientosEn una tienda cuyo billing es "partner", estos dos endpoints están leyendo tu cuenta, así que las filas incluyen movimientos de tus otras tiendas. Esa es la respuesta honesta a “qué se ha movido en el saldo del que gasta esta tienda”, y son los mismos datos que GET /v1/ledger ya le da a la misma llave.?delivery_id= sigue la misma regla: nombra cualquier pedido tuyo en este modo, no solo los de esta tienda. Uno que se cobró a otra cuenta regresa como una página vacía, y uno que no es tuyo es 404 not_found.

Cómo darle acceso al dueño de una tienda

Una tienda dueña de su dinero necesita a alguien que pueda verlo, así que POST /v1/stores/{id}/owner crea el acceso de esa persona a Listo.
Listo le manda por correo una liga para que ella elija su contraseña. Tú nunca ves una contraseña ni un token, y no hay nada que guardar de la respuesta: la credencial existe solo a partir de que esa persona la eligió. Llama otra vez y la misma persona recibe una liga nueva en lugar de un segundo acceso, así que reintentar tras un timeout es seguro, y también lo es reenviar la liga cuando alguien dice que la primera ya venció. La liga dura siete días. Que la dirección ya tenga un acceso a Listo en otro lado no es problema. Esa persona recibe un acceso aparte para esta tienda, con su propia contraseña, y la cuenta que ya tenía queda como estaba. Alguien que lleva dos de tus sucursales puede usar la misma dirección en las dos. Tres rechazos:
  • 422 console_access_required en una tienda cuyo access_tier es api_only. Esa tienda no tiene consola a la cual entrar, así que no hay a qué invitar a nadie. Cambia el tier primero y vuelve a llamar.
  • 422 validation_failed con param: "email" cuando la tienda ya tiene un dueño con otra dirección. El message dice quién es. Una tienda tiene un solo dueño y este endpoint no puede cambiarlo. Para mandarle una liga nueva a ese dueño, llama con la dirección que aparece en el message.
  • 422 validation_failed con param: "email" cuando otra petición está dando de alta esa misma dirección para esta tienda en ese mismo momento. Repite la llamada: para entonces la primera ya terminó y la tuya reenvía la liga.

Cómo fondearlo

Las recargas no son una llamada a la API. Fondea el saldo desde el panel de socios, con tarjeta o transfiriendo a la CLABE permanente que ahí aparece. Esa CLABE no cambia, así que la puedes guardar como beneficiario sin problema. Las dos rutas se diferencian en más que la comodidad, y vale la pena elegir: La comisión de tarjeta es 3.6% más MX$3.00, ambos más IVA, calculados hacia arriba para que el monto que pediste abonar sea el que llega, y no el monto al que se le descontó la comisión. La recarga mínima con tarjeta es de MX$10. Deja un colchón. Una creación cuya parte del negocio no alcanza a cubrir el saldo se rechaza con 402 insufficient_balance y no se despacha nada, así que un saldo vacío detiene pedidos nuevos. Lo que no detiene es todo, y esta es la única forma en que un saldo se va a negativo. El cargo por una cancelación tardía o por una devolución se registra alcance o no alcance el saldo, porque ese viaje ya se manejó y no registrarlo no lo desharía. Así que un pedido cancelado cuando ya hay un repartidor encima, o uno que se regresa tras una entrega fallida, te puede dejar abajo de cero hasta que recargues, y un saldo negativo bloquea la siguiente creación. Es más acotado de lo que suena: no se cobra nada si no hubo un repartidor realmente comprometido, y un pedido que terminó en no_courier no cuesta. Lo que sí se cobra todavía puede regresar. Un viaje cancelado por el repartidor, o por una falla del lado de Listo, es un costo que reclamamos por tu cuenta después del hecho. Cuando eso prospera, llega como un provider_charge_reversal: el mismo monto, abonado completo, contra el mismo pedido. Es una fila aparte y no un encogimiento retroactivo del cargo, así que siempre puedes ver tanto lo que se te cobró como lo que regresó, y puede registrarse semanas después del cargo. No dejes una factura abierta esperándolo, ni des por hecho que un cargo se va a revertir. Cobra el cargo, y traslada el abono a tu propio comercio si llega y cuando llegue. Un saldo que bajó sin que se creara un envío siempre tiene explicación. GET /v1/ledger?type=provider_charge lista cada uno de esos cargos con el pedido del que salió, y ?type=provider_charge_reversal lista los que regresaron.
Vigila el saldo, no los erroresLee GET /v1/balance de forma periódica y alerta contra tu propio piso, en vez de esperar a la primera creación rechazada. Un 402 llega en el peor momento posible: el cliente ya hizo su pedido.

Cómo retirar

POST /v1/withdrawals con un amount_cents. El dinero queda apartado en cuanto la llamada responde: sale de available_cents de inmediato, así que un segundo retiro no puede gastarlo también.
Se queda en pending mientras se gestiona la transferencia, y luego se resuelve en paid o, si la transferencia rebota, en failed, lo cual regresa el monto apartado a tu saldo disponible. Te enteras de cuál por el webhook withdrawal.updated, o leyéndolo de vuelta con GET /v1/withdrawals/{id}.

Cómo recibirlo el mismo día

kind es opcional y vale standard cuando lo omites, que es todo lo anterior: solo tu saldo ya liberado, sin comisión, enviado en 1 a 3 días hábiles. Nada de ese camino cambió, así que una integración escrita antes de que esto existiera sigue pidiendo exactamente lo que siempre pidió. Manda "kind": "same_day" y la transferencia sale el mismo día.
Cuesta una comisión fija, IVA incluido, que la respuesta reporta en fee_cents. La comisión es un SEGUNDO cargo y no un descuento, así que tu banco recibe completo el amount_cents y tu saldo pierde los dos juntos. Concilia un depósito contra amount_cents en los dos tipos; concilia tu saldo contra la suma. fee_cents es 0 en todo retiro standard, así que puedes sumar los dos sin ramificar. Alcanza el efectivo que todavía está en espera, cosa que el camino sin costo no puede, así que aquí el límite no es available_cents. Lo que puede tomar es un tope que calculamos por cuenta y que ya viene con la comisión descontada, y ese tope lo puedes leer antes de pedir: viene en GET /v1/balance. Sale el mismo día, entre las 8 am y las 8 pm del centro de México. Puedes pedirlo a cualquier hora: una solicitud hecha dentro de ese horario vence tres horas después, y una hecha fuera sale a la mañana siguiente. due_at es la fecha límite que tu solicitud realmente lleva, sellada cuando la hiciste y nunca recalculada, para que puedas exigirnos la promesa que te dimos y no una regla que pudo haber cambiado después. Es null en todo retiro standard, porque el camino sin costo promete un rango de días hábiles y no un instante. Hay un mínimo por solicitud y un máximo diario por cuenta, y cada rechazo de abajo nombra su propio límite en message. Tres rechazos pertenecen a este camino: Solo el primero se trata de falta de dinero, y por eso los otros dos no son 402: a quien rechaza el máximo del día puede sobrarle saldo, y a quien rechaza el mínimo le sobra también, solo que está pidiendo muy poco. same_day_cap_exceeded pone param en amount_cents mientras quede espacio en el día y lo deja en null cuando el día ya se agotó, porque en ese estado ningún monto pasa antes de mañana por la mañana. same_day_below_minimum siempre lo pone, porque un número más grande siempre funciona.
Un kind que no reconocemos se rechazaUn kind que no sea ninguna de las dos palabras responde 422 validation_failed con param: "kind". Nunca cae de regreso al camino sin costo: recibir la transferencia lenta y gratuita cuando pediste una del mismo día, sin que nada lo diga, es peor que un error. Un null explícito SÍ es el camino sin costo, porque omitir el campo y mandar null significan lo mismo.

Cómo leer la oferta antes de pedir

GET /v1/balance trae un objeto same_day junto a sus tres cifras. Describe la OFERTA y no un cuarto saldo, así que puedes dibujar la opción en lugar de descubrirla en un rechazo.
ceiling_cents es el amount_cents más grande que aceptaríamos ahora mismo en un retiro del mismo día. No es otro nombre para available_cents. El tope ya trae la comisión descontada, ya está limitado por lo que queda del día y alcanza efectivo en espera que available_cents deja fuera por definición. En una cuenta con efectivo cobrado en la puerta es el número mayor; en una cuenta cuyos cargos rebasaron el saldo es el menor, o cero. Lee el que responde tu pregunta y nunca lo deduzcas del otro. reach_cents es todo lo que el retiro podría tomar antes de la comisión, y advanceable_cents es la parte de eso que sigue en espera. minimum_cents es el mínimo por solicitud, y cap_cents, cap_used_cents y cap_remaining_cents son el máximo diario por cuenta y en dónde vas hoy contra él, para que un tope chico junto a un saldo grande se lea como un límite y no como una falla. offerable es falso cuando ninguna solicitud del mismo día sería aceptada, y unavailable_reason dice en cuál de los dos estados estás: cap_reached, que se libera solo mañana por la mañana, o below_minimum, que no se libera hasta que entre más dinero. Son los mismos dos estados detrás de same_day_cap_exceeded y same_day_below_minimum, así que puedes mostrar el motivo sin gastar una solicitud en averiguarlo. Es null siempre que offerable sea verdadero. window_open dice si estamos enviando en este momento. due_at es la fecha límite que cargaría una solicitud hecha en ese instante y next_window_opens_at es cuándo se reanudan los envíos. La fecha límite que tu solicitud realmente carga es el due_at del retiro que regresa, sellado en ese momento y nunca recalculado.

Cómo casar un pago con tu estado de cuenta

clave_de_rastreo es la clave de rastreo SPEI de la transferencia, el identificador con el que tu propio banco localiza el depósito. Es con lo que se concilia contra un estado de cuenta: el monto se repite entre solicitudes, requested_at es nuestro reloj y no el del banco, y el id wd_ no significa nada fuera de Listo. Siempre viene en el objeto y es null hasta que la transferencia se envía de verdad, así que llega junto con el estado paid en el webhook withdrawal.updated y en cualquier lectura posterior. En un retiro failed se queda en null, porque nunca hubo transferencia.
Es la misma clave que aparece en ese retiro en el panel, así que las dos nunca se contradicen. El comprobante de pago que el equipo adjunta vive solo en el panel: esta API publica la clave, no el documento. Pedir más de available_cents devuelve 402 insufficient_balance y no aparta nada. No hay verbo para cancelar ni modificar un retiro. Una vez solicitado termina pagado o fallido, y los dos desenlaces se te avisan. La transferencia se hace a la cuenta bancaria registrada, que configuras en el panel. Regístrala antes de solicitar tu primer retiro: sin ella esta llamada responde 422 payout_account_not_ready y no aparta nada. El mismo rechazo cubre el día siguiente a que CAMBIES esa cuenta. Un destino recién movido no puede recibir dinero durante 24 horas, que es la ventana en la que todavía se puede detener a alguien que no seas tú moviéndolo, y el message del rechazo dice el instante exacto en que vuelve a poder. Esperar es el único remedio: volver a guardar la misma cuenta reinicia la ventana.

A dónde va el dinero de las tarjetas

Los pedidos con tarjeta no se liquidan por este saldo en absoluto. Lo que el cliente paga por la mercancía se va directo a quien figure como comerciante de registro de esa tienda, la parte de Listo de la tarifa de envío se toma de ese mismo cargo, y nada de eso aparece en balance_cents. El único movimiento de un pedido con tarjeta que vas a ver en el saldo es tu propia parte de la tarifa de envío, cargada al crear como en cualquier otro pedido.