Vende tiempo aire y paquetes de datos de las operadoras mexicanas (Telcel, AT&T, Movistar, Unefon, Bait) contra TAECEL, un agregador PREFONDEADO: tu comercio deposita su saldo directo en TAECEL y Winal orquesta la entrega y contabiliza el consumo — nunca lo custodia (ADR-0001). El flujo son tres pasos: consulta el catálogo de operadoras, actívalas para tu comercio y consulta el catálogo de productos que eso desbloquea, y confirma la recarga a un celular. Por este mismo camino se cobran los recibos de luz, agua, telefonía y TV de paga (3.1) y se consulta su adeudo antes de cobrarlos (3.2): es la ruta REAL del pago de servicios. El módulo Pago de servicios es otra cosa —un simulador de fase 0, bloqueado en producción— y solo sirve para desarrollar.
Textual de TAECEL: «bajo ninguna circunstancia se podrán realizar cancelaciones o
reversos de solicitudes exitosas». No hay refund, no hay
cancel, y no hay excepción por soporte. Si tu product_code o tu
phone_number están mal, el saldo llega igual — a un número que no es el que
querías — y no hay forma de recuperarlo. Valida en tu propio sistema ANTES de llamar a
POST /v1/recharges: Winal valida todo lo que se puede validar (producto,
formato del celular, operadora encendida, cuadre con un cobro enlazado) antes de tocar al
proveedor, pero nada sustituye confirmar el número con tu cliente.
Modo prueba vs. producción: quién entrega la recarga
Con una llave sk_test_ y sin credenciales propias de TAECEL configuradas
en tu tablero (sección Recargas), tus recargas las resuelve el
simulador de Winal: responde al instante con un folio SIMRCG-… y
no gasta saldo real — la API, el catálogo, las comisiones, la idempotencia y los
asientos contables son reales y definitivos, así que integras y pruebas tu flujo completo sin
arriesgar nada. En cuanto das de alta tu llave y NIP de TAECEL —de prueba o de producción—,
Winal ruta tus recargas contra tu propia cuenta en TAECEL: con credenciales de prueba,
contra la plataforma de pruebas de TAECEL (tampoco gasta saldo real); con credenciales de
producción y una llave sk_live_, contra tu saldo REAL.
Esto se impone por código, no solo por convención: POST /v1/recharges con una
llave sk_live_ se rechaza con 400 recharge.livemode_unsupported
antes de reservar nada si el proveedor que atiende a tu cuenta no entrega saldo real
(el simulador nunca lo hace). Da de alta tus credenciales de producción desde tu tablero para
destrabarlo.
Cómo entrega Winal una recarga: dos fases, nunca una
Por dentro, cada recarga contra TAECEL se resuelve en dos llamadas separadas —
RequestTXN para pedir la transacción y StatusTXN para resolverla
con evidencia—, y esa separación es lo que hace posible no duplicar una entrega. Como
integrador no llamas a ninguna de las dos: POST /v1/recharges hace ambas por
ti y te devuelve el resultado que ya tiene, o processing si todavía no lo tiene.
Textual de TAECEL: «toda solicitud en status "En Proceso" genera un cargo y deberá
realizar un request al método StatusTXN para corroborar el status final». Si la
respuesta de TAECEL tarda o se corta, la transacción PUDO haber entrado igual —el saldo se
pudo entregar sin que Winal se enterara a tiempo—, así que Winal nunca da por
fallida una recarga por un timeout local. La deja en processing y la resuelve
después con evidencia real del proveedor (consultando StatusTXN, o
buscándola en su reporte de ventas si el proceso murió antes de guardar su identificador).
Dar por fallida una recarga que sí entró y reintentarla significaría recargar DOS VECES el
mismo teléfono — y eso no se puede deshacer.
Qué significa esto para tu integración: cuando POST /v1/recharges —o un
GET posterior— te devuelve status: "processing",
no la repitas ni la des por perdida. Vuelve a consultar
GET /v1/recharges/{id} pasado un rato (unos segundos suele bastar; el peor caso
documentado por TAECEL son 60 s) hasta ver delivered o failed.
Hoy no hay webhook para el desenlace de una recarga (a diferencia de un
payment_intent): el único camino es consultar. (Sí lo hay para el
depósito que entra a una bolsa: el webhook
recharge.deposit.detected.) La misma
Idempotency-Key con la que la creaste nunca vuelve a pedir la transacción al
proveedor —repetir la petición es exactamente la forma segura de "reintentar" mientras está
processing.
1. Catálogo de operadoras
curl -s https://api.winal.com.mx/v1/recharge-carriers \
-H "Authorization: Bearer $SK"
{
"object": "list",
"data": [
{ "object": "recharge_carrier", "code": "telcel", "name": "Telcel",
"active": true, "category": "Tiempo Aire" },
{ "object": "recharge_carrier", "code": "att", "name": "AT&T",
"active": true, "category": "Tiempo Aire" },
{ "object": "recharge_carrier", "code": "cfe", "name": "CFE (código de barras)",
"active": true, "category": "Servicios" },
{ "object": "recharge_carrier", "code": "netflix", "name": "Netflix",
"active": true, "category": "GiftCards" }
]
}
Es un catálogo global (sin variación por tenant) y se sincroniza solo con el del proveedor: hoy son 172 operadoras y 737 productos. Si una operadora aparece aquí, ya puedes vender sus productos: el paso 2 solo existe para apagar las que no quieras ofrecer.
category es la categoría del proveedor y viaja tal cual — es con la que él
agrupa su propio catálogo, así que agrupar por ella te da la misma vista que el comercio ya
conoce:
category | Qué trae | Operadoras |
|---|---|---|
Servicios | Recibos: luz, agua, TV de paga, telefonía fija. Monto libre. | 88 |
GiftCards | Tarjetas de regalo de denominación fija. | 41 |
Tiempo Aire | Recargas de celular. | 35 |
Paquetes | Paquetes de datos y minutos. | 8 |
logo_url trae el logotipo que publica el proveedor para esa operadora, y cada
producto lo repite en carrier_logo_url para que puedas pintar tu pantalla de venta
sin cruzar dos respuestas. Con 172 operadoras, un nombre en texto obliga a leer — y se lee
despacio con un cliente enfrente.
Esa URL es del proveedor, no nuestra. Puede cambiar o dejar de responder sin aviso, así
que guárdala en tu punto de venta en vez de pedirla en cada venta, y ten un respaldo:
una imagen que no carga no puede impedir un cobro. Llega null en las operadoras
para las que el proveedor no publica ninguna.
La categoría llega null solo si el proveedor no la declara. No la conviertas en un enum de tu
lado: el proveedor puede añadir categorías y una lista cerrada rechazaría operadoras nuevas
que ya podrías estar vendiendo. Agrupa por el texto y deja un grupo para lo que no reconozcas.
Tu tablero (sección Recargas) muestra este mismo catálogo agrupado igual, para que no tengas que llamar a este endpoint solo para verlo.
availability dice cómo se está comportando cada operadora ahora, para que no
ofrezcas en el mostrador lo que va a fallar. Se calcula con las recargas terminales de la
última hora en toda la plataforma —no solo las tuyas: una farmacia que vende dos al día no tendría
muestra para enterarse de que una operadora lleva una hora rechazando— y en el ambiente de tu
llave: lo que pase con recargas de prueba no puede apagarte una operadora en producción, ni al
revés:
| Valor | Qué pasó | Sugerencia |
|---|---|---|
ok | Se está entregando con normalidad. | Ofrécela. |
degraded | Más de la mitad de los intentos recientes fueron rechazados. | Ofrécela con reservas, o baja su prioridad. |
down | Todos los intentos recientes fueron rechazados. | No la ofrezcas hasta que vuelva. |
Con poca muestra responde ok a propósito: no vender por una duda estadística
cuesta más que un rechazo, que el cajero resuelve cobrando otra cosa. Y es una señal, no una
garantía — una operadora en ok puede rechazar la siguiente.
2. Apaga las operadoras que no quieras vender
No hay nada que activar. Tu comercio vende todas las operadoras del catálogo desde el
primer día; este endpoint existe para apagar las que no quieras ofrecer. Una operadora
apagada desaparece de GET /v1/recharge-products y vender contra sus productos
falla con recharge.carrier_not_enabled.
Antes era al revés —un opt-in que arrancaba vacío— y eso dejaba a un comercio con su bolsa conectada y con saldo real sin poder vender nada hasta activar operadora por operadora. El proveedor abre el catálogo completo; el candado lo ponía Winal sin ninguna razón.
curl -s https://api.winal.com.mx/v1/recharge-carriers/telcel/enable \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{"active": false}'
{ "object": "recharge_carrier", "code": "telcel", "name": "Telcel", "active": false }
Manda { "active": true } (o un cuerpo vacío) para volver a venderla. Este
endpoint no exige Idempotency-Key: es una operación de configuración, no una
que mueva dinero. Desde el tablero es un botón por fila, sin llamar a
nada.
3. Productos (montos) disponibles
GET /v1/recharge-products devuelve el catálogo completo —737 productos—,
sin los de las operadoras que hayas apagado en el paso 2.
Dos formas de código conviven, y las dos son estables. Los productos históricos llevan un
código legible (telcel_airtime_5000); los que el sincronizador descubre del proveedor
llevan el código del proveedor como sufijo (att_att070). No es un detalle
estético: dos productos distintos pueden compartir operadora, tipo y precio —el tiempo aire de $50
y un paquete de $50— y un código derivado del monto los colapsaría en uno solo. Trata el
code como opaco: guárdalo tal cual y no lo construyas ni lo interpretes.
Cada producto trae además carrier_name y carrier_category, para que
puedas agrupar tu pantalla de venta por operadora sin cruzar dos respuestas — el cliente dice
«Telcel de cien», no «telcel_airtime_10000». Y kind distingue los cuatro flujos del
mostrador: airtime (acredita un teléfono), data (paquete),
service (recibo de monto libre) y giftcard (entrega un código).
El nombre viene ya legible del proveedor (AT&T $70 · 1.5 GB Navegación), y
merchant_commission_minor llega en cero en los productos descubiertos: la
comisión del comercio no la declara el proveedor, así que el único valor honesto es cero hasta que
exista un acuerdo que la fije.
curl -s https://api.winal.com.mx/v1/recharge-products \
-H "Authorization: Bearer $SK"
{
"object": "list",
"data": [
{
"object": "recharge_product",
"code": "telcel_airtime_1000",
"carrier_code": "telcel",
"kind": "airtime",
"name": "Tiempo aire Telcel $10",
"face_amount_minor": 1000,
"merchant_commission_minor": 30,
"currency": "MXN",
"active": true
},
{
"object": "recharge_product",
"code": "telcel_airtime_2000",
"carrier_code": "telcel",
"kind": "airtime",
"name": "Tiempo aire Telcel $20",
"face_amount_minor": 2000,
"merchant_commission_minor": 60,
"currency": "MXN",
"active": true,
"open_amount": false
},
{
"object": "recharge_product",
"code": "att_att070",
"carrier_code": "att",
"kind": "airtime",
"name": "AT&T $70 · 1.5 GB Navegación",
"face_amount_minor": 7000,
"merchant_commission_minor": 0,
"currency": "MXN",
"active": true,
"open_amount": false
},
{
"object": "recharge_product",
"code": "cfe_bill",
"carrier_code": "cfe",
"kind": "service",
"name": "CFE (código de barras)",
"face_amount_minor": 0,
"merchant_commission_minor": 0,
"currency": "MXN",
"active": true,
"open_amount": true
}
]
}
| Campo | Qué es |
|---|---|
face_amount_minor | Valor nominal de la recarga, en centavos: lo que recibe el celular del cliente y lo que le cobras al pagador. |
merchant_commission_minor | Comisión que tu comercio se queda por vender esa recarga, en centavos — ya incluida dentro de face_amount_minor, no se suma aparte. |
kind | airtime (saldo abierto), data (paquete con vigencia) o service (pago de un recibo: luz, telefonía, TV de paga). |
open_amount | true = el importe lo pone quien paga y por tanto no está en el catálogo: face_amount_minor vale 0 y cada operación manda su amount_minor. Es el caso de todos los recibos. false = denominación fija del catálogo, y entonces mandar amount_minor se rechaza (recharge.amount_not_allowed). |
El costo mayorista que tu comercio le adeuda a la operadora es
face_amount_minor − merchant_commission_minor — no viene como campo aparte
porque es aritmética exacta sobre los otros dos (ver la sección de contabilidad más abajo).
product_code es un código PROPIO de Winal, no el de TAECEL: por dentro,
Winal traduce cada código al del proveedor antes de pedir la transacción, así que tu
integración nunca depende de la nomenclatura interna de TAECEL.
3.1 Pago de servicios: el mismo camino, con el importe del recibo
Un recibo de luz, de telefonía o de TV de paga se cobra por este mismo endpoint y con esta misma máquina de estados. No es una simplificación: un pago de servicio es tan irreversible como una recarga, lo entrega el mismo agregador con las mismas dos fases y por tanto necesita exactamente las mismas garantías de «una sola entrega». Tener dos caminos habría significado construir dos veces la protección que impide cobrar dos veces — y la segunda, sin ejercitarla nunca.
Lo único distinto es el importe: un recibo no tiene denominación, así que su producto lleva
open_amount: true, face_amount_minor: 0 y cada operación manda
amount_minor en centavos.
curl -s https://api.winal.com.mx/v1/recharges \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "product_code": "sky_bill", "reference": "871235412635", "amount_minor": 9500 }'
| Servicio | product_code | Referencia que pide |
|---|---|---|
| CFE (código de barras) | cfe_bill | 30 dígitos del código de barras |
| Telmex | telmex_bill | Número telefónico, 10 dígitos |
| SKY | sky_bill | Número de cuenta, 12 dígitos |
| Megacable | megacable_bill | Número de suscriptor, 10 dígitos |
| Dish | dish_bill | Referencia de pago, 14 o 15 dígitos |
| Maxcom | maxcom_bill | Referencia de pago, 7 dígitos |
No copies esas longitudes a tu formulario. La regla exacta de cada operadora
—etiqueta, longitud mínima y máxima, formato y si admite cero inicial— viaja en
reference dentro de GET /v1/recharge-carriers, sincronizada del
catálogo vivo del proveedor. Es la única fuente que no se desincroniza, y es además la que
de verdad protege: el agregador declara esas reglas y no siempre las aplica al recibir
(medido: aceptó y cobró un número con cero inicial en una operadora que lo prohíbe).
El cargo del proveedor sale de tu bolsa, además del importe. Un recibo de $95.00 con cargo de $5.00 deja tu saldo prefondeado en −$100.00, y así se asienta: el consumo real lo dice la evidencia del proveedor, no el catálogo. Si le cobras al cliente solo el importe del recibo, tu margen en esa operación es negativo por el cargo — y se registra como tal, sin esconderlo. Cobra tu comisión encima si no quieres absorberlo.
3.2 Consultar el adeudo antes de cobrarlo
POST /v1/service-debt-inquiries le pregunta al prestador cuánto debe una
referencia, antes de cobrar nada. Es lo que evita que el cajero teclee el importe a ciegas:
sin esto, la única fuente del monto es el papel que trae el cliente.
curl -s https://api.winal.com.mx/v1/service-debt-inquiries \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{ "product_code": "cfe_bill", "reference": "123456789012" }'
{
"object": "service_debt_inquiry",
"id": "9c1f…",
"status": "succeeded",
"carrier_code": "cfe",
"product_code": "cfe_bill",
"reference": "12******9012",
"amount_minor": 147500,
"currency": "MXN",
"provider_amount_raw": "1475",
"due_at": "2026-09-15T00:00:00+00:00",
"provider_message": null
}
Entre la consulta y el pago el importe puede cambiar: un abono en otra ventanilla, un recargo
por mora, un corte de facturación. Winal no reserva ni congela nada. Por eso el cobro
sigue declarando su propio amount_minor en POST /v1/recharges y
ése es el que se paga — la consulta informa, no autoriza.
Enseña la cifra al cliente y vuelve a validarla al cobrar. Si tu pantalla muestra $1,475.00 y el cargo sale por otra cantidad, el reclamo es tuyo, no del prestador.
Solo algunos prestadores lo admiten, y el catálogo lo dice
La consulta la publica el proveedor por operadora, no por cuenta: hoy la admiten
8 de las 172 del catálogo de producción (CFE por código de barras y por número de
servicio, Megacable, SKY, Star TV, Star Go, Fuller y Pagos Abonos ELEKTRA). El dato viaja en
supports_balance_inquiry dentro de
GET /v1/recharge-carriers — léelo y ofrece el botón de «consultar» solo donde
valga true, en vez de dejar que el cajero descubra a mitad de una venta que
esa operadora no lo ofrece.
Pedirlo donde no aplica responde service_debt.inquiry_not_supported y
no se llama al proveedor. No es un fallo transitorio: para esas operadoras el recibo se
cobra con el importe impreso, como siempre.
Puede tardar: 200 con el adeudo, 202 mientras no se sepa
Quien habla con el agregador es el Worker de Winal, no la API, así que la respuesta llega en un
par de segundos. El POST los espera por ti (6 s por omisión; ajústalo con
wait_ms, máximo 20 000, o pon 0 para no esperar). Si para entonces no
llegó, responde 202 con status: "pending" y su id:
léelo después con GET /v1/service-debt-inquiries/{id}.
Nunca recibes un 200 sin adeudo cuando el adeudo se desconoce. Los dos
hechos —«esto es lo que debe» y «todavía no se sabe»— llegan con códigos de respuesta
distintos a propósito: confundirlos termina en una venta cerrada sin cobrar.
Cuando el prestador no responde, la consulta se falla
Y esto es lo contrario de lo que hace una recarga. Ahí un timeout nunca es un fracaso
(las dos fases): la transacción pudo entrar y repetirla recargaría dos
veces el mismo teléfono. Una consulta no deja nada en vuelo —no crea transacción, no consume
saldo—, así que un timeout se cierra como status: "failed" con
failure_code: "service_debt.provider_unreachable" y reintentar es seguro.
Es la única superficie de este módulo donde eso es cierto.
Límites
- Permiso: exige
recharges:write, el mismo que cobrar. No es lectura de datos tuyos: sale a un tercero con el número de servicio de una persona y cuesta una llamada al agregador. - Sin
Idempotency-Key: no mueve dinero y repetirla no duplica nada. - Tope propio:
service_debt.inquire, 600 por hora de fábrica, ajustable desde tu consola (Topes). Va aparte del de recargas justamente para que consultar no te gaste el cupo de vender. - Por ambiente: una consulta hecha con
sk_test_no se lee consk_live_ni al revés. Y en producción se exige un agregador real conectado: un adeudo simulado en un mostrador se le cobra al cliente como si fuera su recibo. - Cuentas administradas: se consulta con las credenciales de la cuenta EFECTIVA (la que va a pagar el recibo), no las de quien presenta la llave.
provider_amount_raw y por qué te lo publicamos. Es el importe tal como lo
escribió el proveedor, sin interpretar. Winal lo lee como pesos —es la escala que usa esa API
en todo lo demás—, pero el campo llega sin unidad declarada, así que la primera vez que
integres compara este crudo con el recibo de papel del cliente. Si algún día no
cuadran, ahí está el dato para verlo, en vez de descubrirlo por un cargo de cien veces la
cifra correcta.
4. Vender una recarga
POST /v1/recharges ejecuta la recarga contra la operadora. Exige el
header Idempotency-Key (UUID) — igual que toda operación que mueve dinero en
Winal (ver Errores); el endpoint lo valida él mismo, con el mismo
mensaje que el resto de la API.
curl -s https://api.winal.com.mx/v1/recharges \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "product_code": "telcel_airtime_5000", "reference": "5512345678" }'
{
"id": "029fc23d-bef6-477f-a969-8ad8f9c15f6e",
"object": "recharge",
"carrier_code": "telcel",
"product_code": "telcel_airtime_5000",
"phone_masked": "55****5678",
"face_amount_minor": 5000,
"merchant_commission_minor": 150,
"currency": "MXN",
"status": "delivered",
"provider_ref": "SIMRCG-502685aa15034d6ebf935c8a4117432d",
"payment_intent_id": null,
"failure_reason": null,
"created_at": "2026-08-20T04:11:44.083258+00:00",
"updated_at": "2026-08-20T04:11:44.12255+00:00",
"outcome": "delivered",
"settled": true,
"safe_to_retry": false,
"needs_review": false,
"provider_cost_minor": null,
"client_reference": "winal029fc23dbef6477fa9698ad8f9c15f6e",
"provider_transaction_id": null,
"connector_key": "sim",
"livemode": false,
"requested_at": "2026-08-20T04:11:44.083258+00:00",
"attempt_count": 1,
"attempts": [
{ "id": "029fc23d-bef6-477f-a969-8ad8f9c15f6e", "attempt_no": 1, "connector_key": "sim",
"status": "delivered", "provider_ref": "SIMRCG-502685aa15034d6ebf935c8a4117432d",
"provider_transaction_id": null, "provider_cost_minor": null,
"client_reference": "winal029fc23dbef6477fa9698ad8f9c15f6e",
"requested_at": "2026-08-20T04:11:44.083258+00:00", "resolved_at": "2026-08-20T04:11:44.12255+00:00",
"routing": null }
]
}
{
"id": "8a3d0f61-2b4a-4b8e-9c11-0f4e6d2a7b19",
"object": "recharge",
"carrier_code": "telcel",
"product_code": "telcel_airtime_5000",
"phone_masked": "55****5678",
"face_amount_minor": 5000,
"merchant_commission_minor": 150,
"currency": "MXN",
"status": "processing",
"provider_ref": null,
"payment_intent_id": null,
"failure_reason": null,
"created_at": "2026-08-20T11:19:37.0Z",
"updated_at": "2026-08-20T11:19:37.4Z",
"outcome": "undetermined",
"settled": false,
"safe_to_retry": false,
"needs_review": false,
"provider_cost_minor": null,
"client_reference": "winal8a3d0f612b4a4b8e9c110f4e6d2a7b19",
"provider_transaction_id": null,
"connector_key": "taecel",
"livemode": true,
"requested_at": "2026-08-20T11:19:37.0Z",
"attempt_count": 1,
"attempts": [
{ "id": "8a3d0f61-2b4a-4b8e-9c11-0f4e6d2a7b19", "attempt_no": 1, "connector_key": "taecel",
"status": "processing", "provider_ref": null, "provider_transaction_id": null,
"provider_cost_minor": null, "client_reference": "winal8a3d0f612b4a4b8e9c110f4e6d2a7b19",
"requested_at": "2026-08-20T11:19:37.0Z", "resolved_at": null, "routing": null }
]
}
Nota el 201 Created en ambas respuestas: la recarga se creó de todas
formas, esté ya resuelta o no. El código HTTP no te dice el desenlace — el campo
status sí. Y hay un tercer caso que también llega como 201: si
el propio TAECEL rechaza la transacción (una referencia que su catálogo no acepta, un límite
de monto del carrier), la recarga se crea igual con status: "failed" y
failure_reason con el motivo — nunca como un error HTTP, porque la recarga
SÍ se llegó a reservar. Revisa siempre status, nunca solo el código HTTP.
phone_number solo viaja completo en la solicitud; la API jamás lo regresa en
claro. En toda respuesta y en cualquier listado verás phone_masked
(p. ej. "55****5678": dos primeros dígitos + cuatro últimos), nunca el número de
10 dígitos completo.
Si ya cobraste al pagador el valor nominal con un payment_intent propio
(p. ej. le cobraste por tarjeta en tu POS y ahora entregas la recarga con ese dinero),
manda payment_intent_id para enlazar ambos registros. A diferencia de
billers (donde ese campo es de correlación libre), aquí sí se
valida: el intent debe pertenecer a tu tenant, estar en succeeded, y su monto
debe coincidir exactamente con face_amount_minor del producto.
Consultar tu saldo
GET /v1/recharges/balance devuelve el saldo de la bolsa de la que sale tu operación,
leído del agregador. Consúltalo antes de vender si quieres avisar en tu punto de venta
antes de que una recarga falle por saldo.
curl -s https://api.winal.com.mx/v1/recharges/balance \
-H "Authorization: Bearer $SK"
{
"object": "recharge_balance",
"provider_key": "taecel",
"livemode": true,
"uses_own_credentials": true,
"low": false,
"observed_at": "2026-08-22T03:11:15Z",
"pouches": [
{ "pouch_id": "1", "name": "Tiempo Aire", "balance_minor": 530000, "currency": "MXN",
"observed_at": "2026-08-22T03:11:15Z" },
{ "pouch_id": "2", "name": "Pago de Servicios", "balance_minor": 0, "currency": "MXN",
"observed_at": "2026-08-22T03:11:15Z" }
]
}
Seis cosas que conviene leer antes de usarlo:
-
observed_atviaja siempre, y hay que mirarlo. Este saldo es una FOTO, no un contador en vivo: Winal lo refresca cada 15 minutos y al conectar la bolsa. Un saldo sin su hora miente sobre su vigencia. Si necesitas una lectura ahora —la farmacia acaba de reportar un depósito y quiere saber si ya se lo acreditaron—, pídela conPOST /v1/recharge-balance/refresh: lee la bolsa en el momento, con un freno de 10 minutos por bolsa, y deja la foto aquí mismo. Contrato completo en Referencia de API → Depósitos. -
El agregador reparte el saldo en varias bolsas (
pouches) — tiempo aire, pago de servicios, timbres— y cada una gasta lo suyo. Tener saldo en una no habilita vender de la otra: mira la bolsa que corresponde al producto. -
balance_minorpuede ser NEGATIVO. El agregador descuenta el importe MÁS su cargo, así que un recibo de $95.00 con cargo de $5.00 deja −$100.00. No es un error de signo. -
uses_own_credentials: falsesignifica que ese saldo NO es tuyo, sino de la bolsa de quien administra la cuenta que estás consultando. Es el caso normal cuando un integrador centraliza el saldo: no lo presentes como saldo del comercio. Una bolsa de la que la cuenta es dueña —la cuenta madre del integrador, o la cuenta propia de una farmacia— estrue(hasta el 2026-09-24 salíafalsepara cualquier bolsa: ver el cambio). -
refresh(cuando la cuenta vende de una bolsa que se puede leer al momento) dice qué pasó con la última lectura pedida conPOST /v1/recharge-balance/refreshy desde cuándo una nueva llega de verdad al agregador (next_available_at). Se omite con llave de prueba. -
low: trueavisa que alguna bolsa cayó por debajo del umbral que fijó su dueño. Sirve para pintar la alerta en tu punto de venta sin que tú definas el umbral.
El ambiente sale de la LLAVE (sk_live_ o sk_test_), nunca del cuerpo.
Con llave de prueba el saldo es del simulador y no corresponde a dinero real.
Vender por cuenta de un cliente tuyo
Si administras cuentas —un integrador con sus farmacias, cada una con su RFC— la venta va con
dos credenciales: tu Authorization: Bearer dice quién actúa y
Winal-Account-Key dice sobre quién. No viaja ningún identificador de cuenta,
y esa es la idea: un identificador se equivoca al teclearlo, una credencial no.
curl -s https://api.winal.com.mx/v1/recharges \
-H "Authorization: Bearer $SK_DEL_INTEGRADOR" \
-H "Winal-Account-Key: $KEY_DE_LA_FARMACIA" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"product_code": "telcel_airtime_5000", "reference": "5512345678"}'
Todo lo de esta página funciona igual con la segunda credencial: el catálogo, los topes, la contabilidad y el consumo se resuelven contra la cuenta efectiva —la farmacia—, no contra la tuya. De qué bolsa de saldo sale cada una se configura desde tu tablero, en Cuentas de clientes: pueden compartir una sola bolsa o tener la suya.
Los detalles del modelo —cómo se dan de alta, la lista de orígenes permitidos, la firma de petición y los actos que exigen segunda vía— están en Cuentas de clientes.
5. Consultar y listar recargas
curl -s https://api.winal.com.mx/v1/recharges/029fc23d-bef6-477f-a969-8ad8f9c15f6e \
-H "Authorization: Bearer $SK"
curl -s "https://api.winal.com.mx/v1/recharges?limit=50" \
-H "Authorization: Bearer $SK"
Ambos devuelven el mismo shape que la creación (GET /v1/recharges es una lista
envuelta en { "object": "list", "data": [...] }, más recientes primero — una entrada
por pedido, con sus intentos dentro en attempts[]).
?limit= es opcional (default 100, máx. 500). Este GET es tu ÚNICA
forma de saber si una recarga que quedó processing ya se resolvió — ver
Cómo entrega Winal una recarga.
El desenlace: qué hacer después de cada respuesta
Toda respuesta de una recarga trae tres campos pensados para que no tengas que interpretar el estado. Responden la única pregunta que importa en un mostrador: ¿le devuelvo el dinero al cliente, lo intento por otra vía, o espero?
| Campo | Valores | Qué significa |
|---|---|---|
outcome | delivered | Llegó al teléfono. Definitivo. |
failed | No se entregó y no se va a entregar. Es seguro devolver el dinero o intentar por otra vía. | |
undetermined | No se sabe. Ni se reintenta ni se devuelve dinero: el proveedor pudo haberla entregado. Casi siempre es temporal (la recarga sigue en curso); en un solo caso es un pedido ya cerrado que nadie pudo determinar, y ahí needs_review es true. | |
settled | true/false | Si el desenlace ya no va a cambiar. Es true exactamente cuando outcome NO es undetermined. |
safe_to_retry | true/false | Solo es true con outcome: "failed". Programa if (!safe_to_retry) return; y con eso no vuelves a vender de más: un status: "failed" que Winal no pudo determinar trae safe_to_retry: false y te frena. |
needs_review | true/false | Una persona tiene que mirarlo. true en un solo caso: failure_code: "absent_from_report" — el agregador no contestó y la venta no aparece en su reporte, así que lo más probable es que no se registrara, pero no está probado. Comprueba en el portal del agregador antes de volver a vender. Winal sigue buscándola unos días y, si aparece, esa misma recarga cambia sola. |
Un error de red NO es failed. Si tu petición se corta antes de recibir
respuesta, no tienes ningún desenlace: consulta GET /v1/recharges/{id} con el mismo
Idempotency-Key, o repite el POST con esa MISMA llave —te devuelve la
recarga original, nunca una segunda—. Lo que jamás hay que hacer es mandar una llave nueva:
eso sí crea una recarga nueva, y una entregada no tiene reverso.
Estados de una recharge
| Estado | Qué significa | Qué hacer |
|---|---|---|
pending | Reservada (Idempotency-Key, montos y rastro hacia el proveedor ya persistidos) pero nadie la ha pedido todavía — transitorio, dura milisegundos. | Vuelve a consultarla en un momento. |
processing | Ya se pidió al proveedor y todavía no hay evidencia del desenlace. No significa que vaya a fallar: es literalmente lo que dice TAECEL sobre una transacción "En Proceso". También es processing mientras Winal la surte por otro agregador tras un rechazo firme del primero (ver Cuando un agregador falla). | No la repitas. Consulta GET /v1/recharges/{id} pasado un rato hasta ver un estado terminal. |
delivered | La operadora confirmó la entrega del tiempo aire; provider_ref trae su folio. Terminal e IRREVERSIBLE. | Nada — la venta está cerrada. No existe un endpoint de reembolso para esto. |
failed | No se entregó nada y no hubo cargo: rechazo antes de pedir (formato, catálogo), rechazo del carrier al procesar, o la solicitud nunca llegó a registrarse en el proveedor. failure_reason trae el motivo en español y failure_code la clase (not_requested, rejected, failed, absent_from_report, exhausted, unfunded; ver Cuando un agregador falla). Con exhausted, Winal ya la intentó por todos los agregadores que podían atenderla. Terminal, con UNA excepción: absent_from_report llega con needs_review: true y settled: false porque Winal no pudo determinar el desenlace y sigue buscándolo. | Mira primero safe_to_retry. Si es true, corrige lo que indique failure_reason y vuelve a intentar con una Idempotency-Key NUEVA (la anterior sigue apuntando a la recarga fallida). Si es false —solo pasa con absent_from_report—, no vuelvas a vender todavía: comprueba en el portal del agregador. |
Los cuatro estados son EXACTAMENTE los que existen — no hay un quinto "cancelado": cancelar
una recarga entregada no es una operación que la API ofrezca, porque TAECEL no la admite.
Tampoco hay un quinto estado para «se intentó por otro agregador»: eso lo cuenta
attempts[], no status.
Cuando un agregador falla: qué hace Winal por dentro
Una recarga es un pedido tuyo, y Winal puede surtirlo por más de un agregador. Tú no
eliges cuál ni te enteras del cambio: pides un product_code de Winal, y Winal decide
por dónde va — y si el primero no puede, por dónde sigue. Cada intento contra un agregador queda
registrado en attempts[], en orden, con su proveedor, su estado, su transacción y su
costo real; attempt_count dice cuántos hubo. Los campos del proveedor que ya conocías
(provider_ref, provider_transaction_id, provider_cost_minor,
client_reference, connector_key, requested_at) son los del
intento que entregó —o del último, si ninguno entregó—. Para un pedido de un solo intento,
que es el caso normal, ves exactamente lo mismo que antes.
La regla que gobierna el respaldo, y por qué es la única posible. Solo pasa al siguiente agregador un fracaso del que Winal tiene CERTEZA: el agregador lo contestó, o consta que nada se le envió. Ni un timeout ni «no aparece en el reporte» la dan:
| Qué pasó con el intento | attempts[].failure_code | Qué hace Winal |
|---|---|---|
| Falló antes de enviar: el intento se reservó y nunca llegó a pedirse, o las credenciales de su bolsa ya no estaban al armar la petición. Es un estado de Winal, anterior a cualquier llamada. | not_requested | Es un fracaso FIRME: abre el siguiente intento con otro agregador. Si pasó por conciliación, minutos después; tu GET lo verá aparecer en attempts[]. |
| No aparece en el reporte de ventas del agregador pasado el umbral de abandono (30 min): lo más probable es que nunca se registrara y no hubiera cargo, pero no está probado. | absent_from_report | Cierra el intento Y el pedido (status: "failed", failure_code: "absent_from_report") sin abrir otro intento, y lo publica como lo que es: outcome: "undetermined", settled: false, safe_to_retry: false, needs_review: true. La ausencia en un reporte acotado por tiempo no prueba que no se entregó —el reporte se sella con el reloj del agregador, y una venta que sí entró puede caer fuera de la ventana—; pasar a otro agregador ahí serían dos recargas al mismo teléfono, y decirte que es seguro reintentar sería pedirte que las hagas tú. Comprueba en el portal del agregador y solo entonces vuelve a pedirla con una llave nueva. Y Winal no la abandona: sigue leyendo los reportes de los días siguientes (ver abajo). |
| El agregador contestó rechazo (en línea, sin procesar y sin cargo), o aceptó y al consultarlo reportó fracaso con evidencia. | rejected / failed | Es un fracaso FIRME: abre el siguiente intento con otro agregador — en la misma petición si el rechazo fue inmediato, así que tu POST ya vuelve con el resultado del segundo. |
| Timeout, 5xx o respuesta ilegible: la transacción PUDO entrar. | — (el intento sigue processing) | Jamás pasa a otro agregador. Textual de TAECEL: «toda solicitud En Proceso genera un cargo». Pasar a otro ahí serían dos recargas al mismo teléfono, y ninguna se devuelve. El intento se resuelve después con evidencia del proveedor; si esa evidencia es un fracaso firme, entonces —y solo entonces— sigue por otro. |
Lo que no se pudo determinar se vuelve a mirar
Una recarga cerrada con failure_code: "absent_from_report" no es el final de la
historia. Durante los tres días siguientes Winal vuelve a leer el reporte de ventas del
agregador —cada pocas horas, y solo de lectura: nunca le vuelve a pedir la recarga— por si la
venta aparece tarde. Si la encuentra:
| Lo que dice el reporte tardío | Cómo queda la recarga |
|---|---|
| Entregada. El agregador sí la surtió y su reporte la selló fuera de la ventana. | Pasa a delivered sola, con su folio y su costo real, y Winal la contabiliza. Esa misma recarga —mismo id— cambia de status: por eso venía con settled: false. Si ya le habías devuelto el dinero a tu cliente, es el caso que needs_review te pedía revisar. |
| Fracasada. El agregador contestó: no hubo cargo. | Sigue en failed, pero ya con certeza: failure_code pasa a failed, outcome a failed y safe_to_retry a true. Ahí sí, vuelve a venderla con una llave nueva. |
| Nada (no aparece en esos tres días). | Se queda como está, con needs_review: true. Winal no va a afirmar lo que no sabe: la cierras tú, con el portal del agregador a la vista. |
En la práctica esto es raro —hace falta que el agregador acepte la venta, no conteste y su reporte selle tarde—, pero cuando pasa es dinero de tu bolsa: es la diferencia entre enterarte y no.
Dos límites más, escritos en la estructura: nunca el mismo agregador dos veces en el mismo
pedido, y un tope de tres intentos. Cuando tras un fracaso firme no queda ningún agregador
elegible (ya se intentaron todos, o se llegó al tope), el pedido termina con status:
"failed" y failure_code: "exhausted", con el motivo del último intento en
failure_reason: ahí sí, corrige y vuelve a pedir con una llave nueva. Si había otra vía
pero tu cuenta no pudo fondearla (cupo o saldo de esa bolsa), failure_code es
unfunded y el mensaje dice cuál era la vía.
Qué tienes que hacer tú: nada. Sigue programando contra status,
outcome y safe_to_retry como hasta ahora. safe_to_retry es
false mientras Winal tenga un respaldo en curso, justo para que no reintentes por tu
cuenta lo que Winal ya está reintentando. attempts[] es para tu conciliación: cada
intento que llegó al agregador tiene su propia transacción en el reporte de ventas de ese agregador.
{
"id": "5e0f2c1a-7b6d-4c3e-9a21-4f8e1d2b7c90", "object": "recharge",
"status": "delivered", "outcome": "delivered", "settled": true, "safe_to_retry": false,
"connector_key": "mtcenter", "provider_ref": "MTC-88213", "provider_cost_minor": 4850,
"attempt_count": 2,
"attempts": [
{ "id": "5e0f2c1a-7b6d-4c3e-9a21-4f8e1d2b7c90", "attempt_no": 1, "connector_key": "taecel",
"status": "failed", "failure_code": "rejected",
"failure_reason": "El operador rechazó la recarga: número no recargable.",
"requested_at": "2026-09-20T15:02:11Z", "resolved_at": "2026-09-20T15:02:12Z",
"routing": { "reason": "agregadores con credenciales en este ambiente, en orden de preferencia: taecel, mtcenter", "excluded": [] } },
{ "id": "1c9a4b77-0d2e-4f61-b3a5-9e7f6a5d4c21", "attempt_no": 2, "connector_key": "mtcenter",
"status": "delivered", "provider_ref": "MTC-88213", "provider_transaction_id": "88213",
"provider_cost_minor": 4850, "requested_at": "2026-09-20T15:02:12Z", "resolved_at": "2026-09-20T15:02:13Z",
"routing": { "reason": "agregadores con credenciales en este ambiente, en orden de preferencia: mtcenter", "excluded": ["taecel"] } }
]
}
Cómo elige Winal el agregador: los filtros, en orden
Tú pides un product_code de Winal; Winal decide por dónde va. La decisión se toma
en este orden —filtros primero, preferencias después—, y cada paso deja escrito su motivo en
attempts[].routing, en español de mostrador. Una preferencia nunca elige a un
agregador que no puede entregar.
-
¿Puede? Solo entran los agregadores con credenciales alcanzables para tu cuenta en ese
ambiente (las tuyas, o las de la bolsa a la que tu cuenta apunta), que tienen ese producto dado
de alta, y que entregan saldo real si la llave es
sk_live_. El simulador de Winal atiende solo cuando no tienes credenciales de ningún agregador real: jamás es el respaldo de uno real. - ¿Está sano? Winal lleva un cortacircuito por agregador y operadora (Telcel en TAECEL es una cosa; Telcel en MTCenter, otra), con tres estados: cerrado (sano), abierto (fuera del ruteo mientras haya otro) y medio-abierto (pasa una sola recarga de sonda; si entrega, se cierra; si falla, se reabre). Se abre con 3 intentos distintos con fallo del agregador en 10 minutos, siempre que sean al menos 1 de cada 4; se queda abierto 5 minutos. Qué cuenta como fallo del agregador: que no conteste (timeout, 5xx, respuesta ilegible), que no reconozca su propia transacción, o que reporte un fracaso por causa suya (sin enlace con la operadora). Qué NO cuenta: un rechazo por el dato —número inválido, monto fuera de rango, línea suspendida, producto desconocido—; cien de ésos no apagan a un agregador sano. Un cortacircuito abierto nunca deja a tu cuenta sin vender: si es el único agregador que puede, se intenta igual y el motivo lo dice.
- ¿Le alcanza el saldo a la bolsa? Con la última lectura de saldo de cada bolsa, lo ya comprometido que el agregador todavía no cobra y los depósitos que declaraste después —la misma cuenta que hace el techo de la bolsa— Winal comprueba que quepa el costo con margen (10 % del costo, mínimo $5.00, porque el agregador cobra importe más cargo en denominaciones chicas y su lectura de saldo va retrasada). La bolsa que no alcanza no recibe el pedido si otra sí. Sin lectura de saldo, la bolsa entra solo si ninguna otra tiene saldo conocido suficiente.
-
Entre los que quedan, tu prioridad (fijada por operadora, o para todas, en tu tablero →
Recargas → Agregadores). Si no la fijaste, gana el más barato de los sanos, medido con el
único dato real que existe: el último costo que cada agregador te cobró por ese producto
(
provider_cost_minor). Si a alguno todavía no se le conoce el costo, Winal no compara nada —comparar un costo medido contra uno inventado es inventar— y desempata por orden de alta de la bolsa, diciéndolo en el motivo.
La advertencia de las dos bolsas. Con dos agregadores tienes dos saldos que no se mezclan: Winal no mueve dinero entre ellos. Si TAECEL tiene $8,000 y MTCenter $0, MTCenter no entra, aunque lo hayas puesto primero en tu prioridad — y tu tablero te lo dice con esas cifras («¿Por dónde iría ahora?»). Si quieres que un agregador reciba pedidos, deposítale saldo a ÉL. Winal tampoco cambia de agregador a media operación: el intento queda sellado con el suyo.
"routing": {
"reason": "'mtcenter' para el intento 1: 'mtcenter' primero: sano; saldo suficiente: $6,430.00 disponibles para $194.00; más barato de los sanos: $193.20 frente a $194.50 de 'taecel'; 'taecel' no entra: cortacircuito abierto hasta 21:32 UTC por 3 fallos del agregador para 'telcel' en 10 min",
"excluded": [],
"evaluated": [
{ "provider_key": "taecel", "eligible": false, "reason": "cortacircuito abierto hasta 21:32 UTC por 3 fallos del agregador para 'telcel' en 10 min" },
{ "provider_key": "mtcenter", "eligible": true, "reason": "sano; saldo suficiente: $6,430.00 disponibles para $194.00; más barato de los sanos: $193.20 frente a $194.50 de 'taecel'" }
]
}
Idempotencia
Repetir la misma Idempotency-Key con el mismo product_code,
la misma referencia, el mismo amount_minor y el mismo
payment_intent_id devuelve el mismo pedido tal
como está ahora, con todos sus intentos (sin volver a pedir nada a ningún proveedor) — si sigue processing,
la respuesta también es processing; el reintento no la resuelve más rápido, solo
es seguro repetirlo. Con datos distintos bajo la misma llave,
409 recharge.idempotency_conflict — y el importe cuenta como dato: repetir la
llave cambiando amount_minor es un conflicto, no una repetición, porque si no
creerías haber pagado el recibo de $900 cuando el que salió fue el de $90.
Y esa llave es de tu ambiente: se identifica por (tu cuenta, el ambiente de tu
credencial, la Idempotency-Key), no solo por la llave. Tu sk_test_ y tu
sk_live_ no comparten espacio: si derivas la llave del folio de tu ticket —lo
recomendable— y probaste en sandbox con los mismos folios con los que vendes de verdad, tu primera
venta real sí se procesa — la misma llave con tu credencial de producción es una recarga
NUEVA, nunca la repetición de la que hiciste en pruebas ni al revés. Antes de esto, la credencial
productiva podía recibir REPETIDA la respuesta del sandbox (delivered con el folio del
simulador) sin que un peso llegara al teléfono — sobre la única operación de esta página que no
tiene reverso.
Por dentro, Winal también manda una referencia propia (refCte) a TAECEL en cada
solicitud, visible en el portal de TAECEL para rastrear la transacción. No confíes en
ella para deduplicar nada de tu lado: se midió llamando a la API que TAECEL la acepta y
la guarda, pero igual procesa una segunda transacción si se la mandas dos veces — la garantía
de "nunca dos veces" la da la máquina de estados de Winal (tu Idempotency-Key),
no esa referencia.
El permiso que exige una recarga
POST /v1/recharges y POST /v1/service-payments exigen que la llave lleve
recharges:write. No se emite por omisión y solo el dueño de la cuenta puede pedir
una llave con él — el mismo criterio que payouts:write, y por la misma razón: es dinero
que sale y no vuelve. Una llave que no lo lleva recibe:
{
"error": {
"type": "authorization_error",
"code": "insufficient_scope",
"message": "Esta credencial no tiene el permiso 'recharges:write'."
}
}
Se pide al emitir la llave, en winal.com.mx/app →
Llaves de API. Una llave que ya existe también puede ganarlo: Editar permisos sobre
la misma credencial, sin rotarla ni tocar tu sistema — no hace falta redistribuir una llave nueva
solo para vender tiempo aire desde el punto de venta que ya tienes en producción. Lo que NO es
automático es la decisión: añadir recharges:write —a una llave nueva o a una que ya
existe— pide contraseña y segundo factor, y solo el dueño de la cuenta puede hacerlo. Es a
propósito — que una llave viva pueda ganar poder sobre dinero irreversible sin que nadie lo decida
sería justo lo que este permiso viene a impedir.
Topes: el freno que tú ajustas
Una recarga es el único dinero de la plataforma que no se puede devolver: una factura se cancela y una cuenta se revoca, pero el saldo que ya entró a un teléfono ajeno no vuelve. Por eso cada cuenta trae un tope de recargas y otro de pagos de servicio por ventana de tiempo, y a diferencia del resto de los actos se cuentan siempre —también cuando operas con tu propia llave sobre tu propia cuenta—, que es exactamente el caso del comercio al que le roban una credencial.
De fábrica: 200 recargas por hora, 100 pagos de servicio por hora, 600 consultas de adeudo por hora y 20 traspasos de saldo por hora (éste con techo de monto de fábrica además del de número: $2,000,000.00 MXN por hora, porque un traspaso siempre declara cuánto mueve). Están por encima del mejor día de un mostrador y frenan en seco un bucle. La consulta tiene tope PROPIO —y más alto que el del pago que la sigue— para que consultar no te gaste el cupo de vender: un cajero consulta varias veces por venta, y hay quien consulta sin llegar a pagar. Los actos que rechaza el propio tope no cuentan para la ventana, así que un reintento no te mantiene la cuenta cerrada. Una petición que falla más adelante (referencia inválida, producto inexistente) sí gastó su lugar: el tope cuenta intentos.
Ajústalos a lo que de verdad vendes desde winal.com.mx/app → Seguridad → Topes de actividad (los tuyos) o Cuentas de clientes → Configurar → Topes de actividad (los de cada cliente que administras). Se cambian desde la consola, que se abre con contraseña y segundo factor, nunca con la API key: si la misma credencial que el tope acota pudiera levantarlo, el tope no serviría de nada.
Sobre el techo de monto, sin adornos: solo puede aplicarse cuando la petición
declara el importe (amount_minor), y con un producto de denominación fija el
cuerpo no lo lleva —la API lo rechaza— así que ahí el acto cuenta únicamente por número. No lo
tomes como un límite de dinero: los límites de dinero son otros dos, y están más abajo
(Los dos techos del dinero). El tope de actividad es el freno de ritmo,
que es lo que aquéllos no dan.
Cuando un tope frena algo, la respuesta es 429 con
account.activity_limit_reached, los números exactos (cuántos actos, en cuántos minutos,
contra qué tope) y un Retry-After que mide lo que falta para que se libere el primer
lugar, no la ventana entera.
Esa petición no se ejecutó — y eso no es lo mismo que decir que no pasó nada. Si venías
reintentando una recarga anterior, repite con la MISMA Idempotency-Key: un
reintento así no gasta cupo, no se frena nunca y te devuelve la recarga original. Lo que no debes
hacer es cobrar de nuevo con una llave nueva; eso sí sería una segunda recarga, y no tiene reverso.
Ver Errores → Topes de actividad.
Los dos techos del dinero: el cupo y el saldo
El tope de actividad frena el ritmo. El dinero lo frenan otras dos cosas, y conviene no confundirlas porque se levantan de formas distintas.
| Techo | Qué acota | Cuándo salta | Cómo se levanta |
|---|---|---|---|
El cuporecharge.allowance_exhausted |
Tu parte de una bolsa que es de otro (la del integrador, la del grupo). | Cuando ya gastaste todo lo que te asignaron. Hay dinero en la bolsa; no es tuyo. | Quien administra la cuenta le acredita un depósito más (Recargas → la cuenta → Asignar depósito). El saldo de las demás cuentas no se toca. |
El saldorecharge.bag_balance_insufficient |
El bolsillo de la bolsa contra el que consume ese producto: el dinero que de verdad hay en el agregador para ESE tipo de venta. | Cuando ese bolsillo no da para esa recarga. | Depositando en el agregador. Si repartes la bolsa, además regístralo en Recargas → Depósitos para que cuente de inmediato. |
La dudarecharge.bag_balance_unknown |
Lo mismo, cuando Winal todavía no ha podido leer ese saldo. | En producción, si la bolsa no está dada de alta o nunca se ha leído su saldo. En modo prueba no aplica. | Dando de alta la bolsa en Recargas → Bolsas (se verifica al guardarla y su saldo
aparece en segundos). El propio rechazo vuelve a pedir esa lectura: reintenta con la
misma Idempotency-Key. |
Cada vínculo tiene uno de los dos, siempre. Si consumes una bolsa que no es tuya, tu cuenta
nace con cupo y no puede gastar hasta que su dueño te asigne saldo: es lo que impide que una
sola cuenta se lleve el de todas las que comparten la bolsa. Y ese cupo no se puede apagar mientras
la bolsa sea ajena (recharge.allowance_cap_required). Si la bolsa es tuya no hay nada
que repartir, y tu techo es directamente su saldo.
Cómo calcula Winal el saldo, dicho sin adornos. No llama al agregador en cada venta: usa la
última lectura de su saldo, le resta todo lo comprometido que el agregador
todavía no ha cobrado de esa cifra y le suma los depósitos que hayas registrado con
fecha posterior. «Comprometido» son las tres cosas: lo entregado, lo que está en vuelo —que el
agregador ya cobró aunque no conste que llegó— y lo que aceptamos y todavía no hemos pedido.
Esto último importa más de lo que parece: la API no habla con el agregador (lo hace el Worker,
segundos después), así que toda recarga pasa por ese estado, y no contarla dejaba pasar tantas
ventas como cupieran entre dos lecturas del saldo. Y una recarga deja de restar cuando llega una
lectura posterior a su desenlace, que es la primera que puede traerla ya descontada: una
lectura nueva no borra lo que sigue pendiente, por muy vieja que sea la venta. El
mensaje del rechazo trae las cifras y la hora de la lectura, para que se pueda comprobar. Es una
estimación deliberadamente conservadora, y el rechazo pide una relectura del saldo: si acabas
de fondear, reintenta en unos segundos con la misma Idempotency-Key.
El saldo se compara por BOLSILLO, no por bolsa. Una cuenta del agregador tiene varios bolsillos con saldo propio que él no mezcla —tiempo aire, pago de servicios, timbres— y cada producto consume del suyo. Winal compara contra el del producto que estás vendiendo: tener $5,000 en pago de servicios no te deja vender tiempo aire si esa bolsa está en cero. A qué bolsillo pertenece cada producto lo dice el catálogo del proveedor, no una regla nuestra; mientras un producto no lo declare, se compara contra la bolsa entera.
Si Winal nunca ha podido leer el saldo de tu bolsa, en producción no se vende
(recharge.bag_balance_unknown). Antes se dejaba pasar, con el argumento de que el techo
de verdad era el cupo; eso es falso justo donde esta guarda es el único techo: el vínculo contra tu
propia bolsa no lleva cupo por diseño, y ahí «no sé cuánto hay» era literalmente «sin techo».
No es un callejón: dar de alta la bolsa la verifica y escribe su saldo en segundos, y el propio
rechazo vuelve a pedir esa lectura. En modo prueba no aplica: ahí el saldo no es dinero.
Tres formas de manejar el saldo de una bolsa
Si administras cuentas de clientes, el saldo de recargas se puede organizar de tres maneras. Las tres salen del mismo mecanismo —la bolsa y el vínculo— y lo único que cambia entre ellas es quién acredita el cupo de cada cliente. Ninguna cambia quién puede gastar: eso lo decide el cupo, y sigue donde estaba.
| Modo | Cómo entra el dinero | Quién acredita | Cuándo conviene |
|---|---|---|---|
Global, manualglobal_manual |
Tus clientes depositan todos a tu cuenta del agregador, sin distinguirse. | Tú, mirando el comprobante que te manda cada uno (Recargas → la cuenta → Asignar depósito). | Es el valor por omisión y funciona desde siempre. Con pocos clientes, es suficiente. |
Global, por referenciaglobal_by_reference |
Igual: todos depositan a la misma cuenta, pero cada uno con su propia referencia, que el agregador te devuelve en el movimiento. | Winal: reconoce la referencia y propone (o acredita) el cupo del cliente dueño. | Con decenas de clientes. Es lo que convierte «entraron $50,000» en «la farmacia de Tlalpan depositó $5,000», sin abrirle una cuenta a cada una ante el proveedor. |
Una bolsa por clienteper_account |
Cada cliente tiene su propia cuenta en el agregador y deposita a la suya, con su referencia, y reporta su depósito en el formulario del agregador. | Nadie: no hay nada que repartir. Su techo es su saldo real. | Con TAECEL es el camino nativo: Winal le abre esa cuenta a cada cliente desde su ficha (ver La cuenta propia de cada farmacia). La bolsa que nace así ya está en este modo. |
El modo se elige por bolsa en Recargas → Depósitos de tus clientes → Modo de saldo de cada bolsa. Cambiarlo no mueve un peso ni toca los cupos ya asignados.
Con TAECEL, el camino nativo es la cuenta propia de cada farmacia. El agregador ya resuelve por su lado lo que los dos modos «globales» construyen por fuera: con su alta de cuentas, cada farmacia tiene su referencia, su formulario para reportar el depósito y su saldo, y nadie tiene que adivinar de quién es un depósito ni repartir cupo. Los modos globales siguen existiendo —para un agregador que no abre cuentas, o si prefieres operar una sola bolsa— y no cambian en nada.
La referencia de depósito
Es el código que tu cliente escribe en su ficha y que el agregador te devuelve en el movimiento. En Winal la das de alta por cliente y por bolsa, en la misma pantalla. Reglas:
- Una referencia es de UN solo cliente dentro de una bolsa. Si fuera de dos, cada depósito
que llegara con ella quedaría ambiguo y nadie cobraría su saldo
(
recharge.deposit_reference_taken). - Ni una puede estar CONTENIDA en otra de la misma bolsa
(
recharge.deposit_reference_collides):7001y70012345no pueden convivir. El banco escribe estas referencias con guiones o espacios y Winal admite esa forma a propósito, así que un depósito de 70012345 escrito «7001-2345» casa con la referencia 7001 — y con ninguna más. No sale ambiguo: sale atribuido al cliente equivocado. Se rechaza al darla de alta, que es el único momento en el que hay alguien delante para elegir otra. - Un cliente puede tener varias: el agregador emite una referencia por bolsillo (una para tiempo aire y otra para servicios), y las dos son suyas.
- Se guarda sin espacios ni guiones y en mayúsculas:
88-799-629y88799629son la misma. Mínimo 4 letras o dígitos (recharge.deposit_reference_too_short): una más corta casaría con demasiados depósitos, y casar de más es acreditarle a un cliente el dinero de otro. - La referencia tiene que estar en la bolsa de la que ese cliente consume
(
recharge.deposit_reference_bag_mismatch). Si tienes dos bolsas en el mismo agregador, un depósito que cae en una no puede dar saldo gastable en la otra. - Archivar es la marcha atrás y no pide segundo factor: es lo que se hace cuando la referencia quedó en el cliente equivocado. Una referencia archivada se puede volver a dar de alta para otro cliente.
El modo sombra, y por qué existe
Winal empieza PROPONIENDO, no acreditando. Una bolsa en modo «por referencia» nace con la acreditación automática apagada: el sistema reconoce el depósito, te dice de quién cree que es y en qué campo del movimiento encontró la referencia, y tú confirmas con un clic. El motivo es un hecho y no una precaución: el contrato del agregador publica un solo ejemplo de su reporte de movimientos, y en qué campo viaja la referencia de un depósito real todavía no está medido. Acreditar sobre una suposición regala saldo que se gasta en recargas que no se devuelven; proponer sobre una suposición cuesta un clic. Cuando tus primeros depósitos reales confirmen el campo —la bandeja te lo dice por cada uno—, enciendes la acreditación automática y dejas de confirmar.
Encender la acreditación automática pide contraseña y segundo factor: es el único estado en el que el cupo de un cliente sube sin que nadie mire. Apagarla no los pide, a propósito: poner un trámite en el camino de salida de un control consigue que nadie lo use.
La bandeja: propuestos, sin dueño y ambiguos
Todo abono que entra a una bolsa en modo «por referencia» acaba en la bandeja (Recargas → Depósitos de tus clientes) con uno de estos desenlaces. Nunca se acredita ante la duda, y las dos dudas son distintas:
| Estado | Qué pasó | Qué hacer |
|---|---|---|
| Propuesto | Casó exactamente una referencia. El importe que se acreditaría es el del abono, tal como lo reportó el agregador; tu comisión se aplica encima, igual que en el camino manual. | Confirmar (pide contraseña y segundo factor). Con la acreditación automática encendida no aparece: se acredita solo. |
| Sin dueño | Ninguna referencia vigente casó. Entró dinero y el sistema no sabe de quién es, así que no le subió el cupo a nadie: quien depositó no puede vender. | Dar de alta la referencia que falta —el depósito que ya llegó se reevalúa solo en el siguiente barrido— o Asignar el depósito a mano (el mismo formulario crea la referencia, si quieres, para que los siguientes se reconozcan solos). |
| Ambiguo | Casaron las referencias de dos clientes distintos en el mismo movimiento. Winal no elige: elegir sería regalarle el dinero de un cliente a otro. No puede ser «la misma referencia dada de alta para dos clientes» —eso lo impide el índice único de la bolsa—: es un dato del movimiento en el que casan dos etiquetas distintas. | Mira la columna Campo (dice con qué valor casó) y Asigna el depósito a mano. Si una de las dos referencias ya no va, archívala: el depósito se reevalúa solo en el siguiente barrido. |
Un depósito no se acredita dos veces, nunca. El reporte del agregador se relee cada ciclo (el
día de ayer y el de hoy), así que el mismo abono se ve muchas veces; lo que lo impide es que cada
movimiento tiene una sola fila de atribución, en la base, incluida la de los que rechazaste
—un rechazo que no se recuerda sería un bucle—. Y cuando el abono se acredita, el depósito que se
crea lleva por comprobante el propio movimiento (mov:<agregador>:<id>), que
es la segunda barrera contra lo mismo.
Un importe que no se puede leer no se propone. Si el agregador reporta el monto de un abono de
una forma que Winal no puede interpretar como dinero, el depósito llega igual a la bandeja —con el
texto crudo a la vista— en vez de desaparecer o convertirse en cero. La salida es Asignar
declarando el importe: el formulario pide el monto solo para esas filas, y por API es
amount_minor en el cuerpo de
POST /app/api/recharge-deposit-attributions/{id}/assign. Sobre un abono cuyo importe
sí se leyó, ese campo se rechaza
(recharge.attribution_amount_not_editable): Winal transcribe el abono del agregador, y lo
que se elige al asignar es de quién es el depósito, no cuánto entró. El importe que pone una
persona se guarda aparte del que reportó el agregador — el hecho no se reescribe, se resuelve.
Un abono de MODO PRUEBA nunca acredita cupo (recharge.deposit_test_movement). Sale
en la bandeja marcado Prueba —sirve para comprobar que el casador reconoce tu referencia sin
estrenarlo con dinero— y ahí se queda: el cupo de recargas no distingue ambiente, así que
acreditarlo daría saldo real, gastable en recargas que no se devuelven. La consola no ofrece el botón
para esas filas; sí se pueden rechazar para limpiar la bandeja.
Y no se acredita contra una bolsa cuyo saldo Winal nunca leyó
(recharge.deposit_bag_balance_unknown). Sin una lectura del agregador no hay contra qué
comprobar que el dinero exista, y «sin cifra no hay techo» es exactamente cómo se reparte saldo que no
está: el propio rechazo vuelve a pedir la lectura, que llega en segundos.
La bandeja vacía te dice de cuál de las dos cosas se trata. «No llegó ningún depósito» y «todavía no hemos leído un solo movimiento de esa bolsa» se ven idénticos y significan lo contrario, así que la pantalla avisa cuando es lo segundo. Si acabas de dar de alta la bolsa, es normal unos minutos; si lleva horas, revisa sus credenciales.
Sin custodia, como todo lo demás. El dinero está en el agregador, a tu nombre; lo que Winal escribe es de quién es cada peso de esa bolsa. Es una obligación tuya con tu cliente, no de Winal con nadie.
La cuenta propia de cada farmacia en el agregador
Si administras cuentas de clientes, puedes darle a cada una su propia cuenta en el agregador
(TAECEL: RegistroCuenta), dentro de tu red de distribuidor, sin salir de tu consola:
Cuentas de clientes → la cuenta → Su cuenta en el agregador. Con ella, la farmacia:
- deposita con SU referencia —la que le asigna el agregador— directo a la cuenta del agregador, sin pasar por la tuya ni por Winal (sin custodia);
- reporta su depósito en el formulario del propio agregador (sube su comprobante): el enlace
viene en su ficha y por
GET /v1/recharge-subaccountsyGET /v1/recharge-report-links, y el agregador lo publica como incrustable en tu punto de venta —el aviso de que el abono entró (recharge.deposit.detected) y el saldo al momento están en Referencia de API → Depósitos—; - vende de SU saldo: su bolsa es suya (
per_account), su techo es lo que ella deposita y nadie tiene que repartirle cupo.
Pedir el alta
Datos del titular —nombre, apellidos, correo y teléfono de 10 dígitos— y, opcional, el nombre
comercial. La pide el dueño de tu cuenta con contraseña y segundo factor: crea una cuenta
en un tercero a nombre de otra persona. Queda en la bitácora de la cuenta del cliente (con el teléfono
enmascarado; el titular jamás se escribe en claro en ningún registro). Nunca es automática y lleva
un tope de 30 altas por hora por distribuidor (recharge_subaccount.rate_limited), para
que una sesión robada no pueda sembrar cuentas en masa.
- Hacen falta TUS credenciales de distribuidor de producción: la cuenta nueva nace dentro de tu red. Registra antes tu bolsa en Recargas → Bolsas.
- «Activarla en el acto» viene marcado, y conviene: el agregador activa la cuenta y le manda al titular sus credenciales por correo en ese momento. Desmarcado, la cuenta queda pendiente y el titular recibe un código para activarla él — hasta entonces no puede depositar ni vender.
- El teléfono es el número de la cuenta en el agregador y no se puede repetir.
- El agregador exige que tu cuenta de distribuidor tenga una comisión por defecto asignada; si
no, rechaza el alta sin crear nada (
recharge_subaccount.no_default_commission) y se pide a él.
El alta la ejecuta Winal en segundos. Si el agregador contesta con las credenciales de la cuenta nueva,
se guardan cifradas —nadie las vuelve a ver, ni tú— como la bolsa propia de la farmacia; se
verifica su saldo y queda created con su referencia y su enlace de reporte. Si la farmacia
no vendía de ninguna bolsa, queda vendiendo de la suya; si vendía de tu bolsa compartida, sigue
ahí hasta que pulses «Que venda de su cuenta» (una cuenta recién creada nace en cero).
Si el alta queda sin desenlace
indeterminate —«sin desenlace»—, nunca «falló», y jamás la vuelve a pedir sola:
darla por fallida invitaría a pedirla otra vez, o con otro teléfono, y la farmacia quedaría con
dos cuentas.
Sus salidas están en la ficha, y todas existen de verdad:
- Registrar sus credenciales: si al titular le llegó el correo del agregador con su llave y su
NIP, regístralas en la ficha. Resuelven la misma alta —no crean otra—, se guardan cifradas y la
cuenta queda
created; Winal lee su enlace de reporte con esas credenciales. Tienen que ser de esa cuenta: con otro teléfono se rechaza (recharge_subaccount.phone_mismatch_conflict), y con otro número de cuenta que el que dio el agregador, también (recharge_subaccount.provider_account_mismatch_conflict). - Reintentar con los mismos datos: si no le llegó nada, reintenta. El reintento no recibe
datos —copia el teléfono y el titular del alta original—, así que no puede ir con otro teléfono ni
por error. Si la primera llamada sí creó la cuenta, el agregador contesta que el teléfono ya existe
(
recharge_subaccount.phone_taken): casi seguro es la prueba de que la cuenta existe, y la salida es la primera. - Darla por no creada: solo si el agregador te confirma (su portal de distribuidor o su
soporte) que no existe ninguna cuenta con ese teléfono en tu red — por ejemplo, porque el teléfono
estaba mal y es de otra persona. Lo afirma el dueño con escalón y queda en la bitácora; es la
única forma de volver a pedirla con otro teléfono (
recharge_subaccount.confirmed_not_created).
indeterminate, con
el motivo del reintento y las mismas salidas. Un reintento frenado por el tope de altas no toca nada: el
alta sigue sin desenlace y se reintenta al liberarse.
Un alta que se queda en cola (nadie la atendió en un par de minutos) muestra cuánto lleva así y ofrece volver a encolarla, cancelarla —nadie llamó al agregador, así que no se creó nada— o registrar las credenciales de la cuenta que la farmacia ya tuviera.
Si el agregador dijo que la creó pero no devolvió las credenciales, queda
awaiting_credentials: se sabe que existe, y la única salida es registrarlas cuando le
lleguen al titular. Y si la farmacia ya tenía su cuenta en el agregador antes de Winal, no hace
falta pedir nada: registra sus credenciales directamente — sirve también con un agregador que no abre
cuentas por API.
En tu punto de venta
GET /v1/recharge-subaccounts con la credencial de la farmacia
(Winal-Account-Key) devuelve su referencia de depósito, su enlace de reporte y su número
de cuenta en el agregador — de esa farmacia y de ninguna otra. Es de solo lectura y nunca trae
credenciales. Contrato en Referencia de API → La cuenta propia
de cada cliente. El enlace de reporte, el aviso de que el depósito entró
(recharge.deposit.detected) y el saldo al momento están en
Referencia de API → Depósitos.
Si tu cuenta administra cuentas de clientes, las guías de operación están en tu consola, en Guías.
Fondearla desde tu bolsa
Un traspaso con destination_account = la farmacia llega a
su cuenta del agregador: Winal toma su número de cuenta del alta (cuentaID). El
cuadre de la bolsa de la farmacia ve ese traspaso como dinero que entró —aunque lo ordenaste tú— y el
conciliador lo cierra con los movimientos de su cuenta. El traspaso llega a su cuenta propia
aunque todavía venda de tu bolsa compartida: así se fondea antes de pasarla a vender de ella. Si
la registraste a mano sin su número de cuenta, ponlo en la ficha: sin él no hay a quién mandarle el
saldo.
El número de cuenta decide a dónde llega un traspaso, y un traspaso no se deshace. Por eso solo el
dueño de tu cuenta lo fija o lo corrige, con escalón; el que devolvió el agregador no se pisa en
silencio (la ficha dice quién lo cambió, cuándo y desde qué valor), y una misma cuenta del agregador no
puede estar en dos bolsas vivas de tu red (recharge_subaccount.provider_account_conflict).
Lo que todavía no está medido, dicho como tal. Que el clienteID que TAECEL espera
en un traspaso sea el cuentaID que devuelve su alta de cuentas es una lectura de su
documentación, no una medición: la cuenta de pruebas con la que se mide el traspaso aún no tiene una
segunda cuenta a la cual mandarle saldo. Hasta medirlo, un traspaso que TAECEL rechace por cuenta
destino desconocida lo hace sin mover nada (recharge_transfer.provider_rejected).
Contabilidad: sin custodia, suma cero
Winal nunca retiene el dinero de una recarga (ADR-0001, sin custodia). TAECEL es un agregador PREFONDEADO: tu comercio deposita su saldo directo en TAECEL y una recarga simplemente consume ese saldo ya existente — no es un pasivo que Winal deba saldar después. Al confirmarse la entrega, el valor nominal cobrado al pagador se reparte por partida doble entre el consumo de ese saldo y la comisión que gana el comercio. Una recarga de $50.00 con comisión de $1.50 genera este asiento, exacto al centavo:
| Cuenta | Movimiento | Qué representa |
|---|---|---|
recharge_clearing | +50.00 (cargo) | Valor nominal entregado al celular del cliente. |
recharge_prepaid:taecel | −48.50 (abono) | Saldo prefondeado consumido en TAECEL. |
merchant_recharge_income | −1.50 (abono) | Comisión que gana tu comercio por la venta. |
La suma es cero: 50.00 = 48.50 + 1.50. El COSTO del asiento sale de la
evidencia que reporta TAECEL para esa transacción exacta —no del catálogo—, así que
un margen distinto del esperado se refleja tal cual (incluso NEGATIVO, si el costo reportado
superó el nominal: se asienta como un cargo a merchant_recharge_income en vez de
esconderse). El fee de Winal jamás entra en este flujo — se factura aparte, igual que
en todos los demás módulos de la plataforma.
Validación de la referencia (el celular, o el número del recibo)
Antes de pedir cualquier transacción, Winal comprueba dos cosas y en este orden. Primero, que
la referencia sea almacenable: letras, dígitos o @ . _ -, sin espacios y
hasta 64 caracteres (recharge.invalid_phone si no). Después —y esta es la que de
verdad protege— la valida contra la regla EXACTA que el catálogo VIVO del proveedor declara
para esa operadora: longitud mínima y máxima, formato y si admite empezar en cero
(recharge.invalid_reference, con el motivo concreto en el mensaje).
La primera comprobación dejó de ser «10 dígitos» a propósito. Mientras lo único que se vendía era tiempo aire, «diez dígitos» parecía una guarda razonable; en cuanto entró el pago de servicios pasó a ser una puerta que ninguna referencia legítima de recibo puede cruzar — CFE pide treinta caracteres y Maxcom siete. La longitud correcta la dice el catálogo del proveedor, no una constante nuestra.
Y la segunda validación importa más de lo que parece: se midió que TAECEL no aplica su propia regla en la recepción — aceptó y cobró una recarga de Telcel a un número que su propio catálogo prohíbe. Es la única barrera real contra una operación bien formada mandada a la referencia equivocada, y esa no se devuelve.
Errores de recharge-carriers / recharge-products / recharges
Guía completa (incluida la activación de operadoras y el catálogo de productos) arriba. Catálogo de códigos:
| HTTP | code | Causa |
|---|---|---|
400 | recharge.invalid_carrier | Falta el código de operadora en /enable. |
404 | recharge.carrier_not_found | El código de operadora no existe o está inactivo. |
400 | recharge.livemode_unsupported | La API key es sk_live_ y el proveedor que atiende a tu cuenta (el simulador, sin credenciales reales de TAECEL) no entrega saldo real — conecta tu cuenta de TAECEL en producción o usa una llave sk_test_ mientras tanto. |
400 | recharge.missing_fields | Falta product_code o el destino (reference, o el histórico phone_number). |
400 | idempotency_key_required | Falta el header Idempotency-Key o no es un UUID válido. |
404 | recharge.product_not_found | product_code no existe o no está activo. |
400 | recharge.invalid_phone | La referencia no es almacenable (espacios, caracteres raros, más de 64 caracteres). |
400 | recharge.amount_required | El producto es de monto libre y falta amount_minor (o llegó en cero o negativo). |
400 | recharge.amount_not_allowed | El producto tiene denominación fija y aun así llegó amount_minor. |
400 | recharge.carrier_not_enabled | La operadora del producto no está activada para tu tenant — actívala primero (paso 2). |
400 | recharge.product_not_supported_by_provider | El producto existe en el catálogo de Winal pero no está dado de alta con el agregador que atiende a tu cuenta. Se rechaza ANTES de pedir nada: no hubo cargo. |
404 | recharge.payment_intent_not_found | payment_intent_id no existe o no pertenece a tu tenant. |
400 | recharge.payment_intent_not_succeeded | El payment_intent enlazado no está en succeeded. |
400 | recharge.payment_intent_amount_mismatch | El monto del payment_intent no coincide con el nominal de la recarga. |
409 | recharge.idempotency_conflict | Misma llave, datos distintos. |
404 | recharge.not_found | El id no existe (o es de otro tenant). |
400 | recharge.invalid_period | El from/to no es una fecha válida o el rango está invertido. Ojo con el + de un desfase horario en la URL: hay que escaparlo como %2B (o mandar la fecha en Z), porque un + sin escapar se decodifica como espacio. |
400 | recharge.invalid_cursor | El cursor no es uno emitido por esta API. Usa tal cual el next_cursor de la página anterior; no lo construyas. |
Estos DOS solo aparecen del lado del proceso que ejecuta, nunca como respuesta directa a tu
petición — si los ves reflejados en un failure_reason, es un problema de
despliegue de Winal, no de tu integración: recharge.provider_unavailable (el
agregador de esa recarga no está conectado en el proceso que la tomó) y
recharge.reference_mismatch (guarda interna: el destino no coincide con el de la
reserva). Catálogo completo, con fila y ancla, en Errores.
{
"error": {
"type": "invalid_request_error",
"code": "recharge.missing_fields",
"message": "Se requieren 'product_code' y el destino ('reference', o 'phone_number' para un celular).",
"doc_url": "...",
"request_id": "..."
}
}
{
"error": {
"type": "invalid_request_error",
"code": "recharge.livemode_unsupported",
"message": "Las recargas en modo producción requieren un agregador real conectado; el proveedor 'sim' solo simula la entrega. Usa una llave de prueba (sk_test_) o da de alta las credenciales del agregador.",
"doc_url": "...",
"request_id": "..."
}
}
{
"id": "5c3a9e2f-1d4b-4a9e-9b7f-2e8f0c6d4a1b",
"object": "recharge",
"status": "failed",
"failure_reason": "«Numero Celular» no puede empezar con cero.",
"provider_ref": null,
"…": "…"
}
Endpoints
| Método | Ruta | Notas |
|---|---|---|
| GET | /v1/recharge-carriers | Catálogo global de operadoras. |
| POST | /v1/recharge-carriers/{code}/enable | Enciende o apaga una operadora; el default es venderla. No exige Idempotency-Key. |
| GET | /v1/recharge-products | Solo productos de operadoras activadas por tu tenant. |
| POST | /v1/recharges | Requiere Idempotency-Key. Devuelve 201 aunque el desenlace sea processing o failed. |
| GET | /v1/recharges | ?limit= opcional (default 100, máx. 500), ?from=/?to= ISO-8601 y ?cursor=. Devuelve has_more y next_cursor. |
| GET | /v1/recharges/statement | Estado de cuenta del período: entregado con costo real, en vuelo, rechazado, pagos de servicio, abonos, cupo y —si la cuenta es dueña de la bolsa— su saldo. Sin fechas, los últimos 30 días. |
| GET | /v1/recharges/statement.csv | El mismo período, una línea por movimiento, para cruzarlo en una hoja de cálculo. |
| GET | /v1/recharges/{id} | 404 recharge.not_found si no existe. Tu ÚNICA forma de saber si una processing ya se resolvió. Trae attempts[]: cada intento contra un agregador, con su desenlace y por qué fue ahí. |
| GET | /v1/recharges/balance | Saldo de la bolsa de la que sale tu operación, con la hora en que se leyó. |
| POST | /v1/service-debt-inquiries | Consulta el adeudo de un recibo antes de cobrarlo. No exige Idempotency-Key. 200 con el adeudo, 202 si todavía no se sabe. |
| GET | /v1/service-debt-inquiries/{id} | Relee una consulta que salió 202. 404 service_debt.not_found fuera del ambiente de tu llave. |
Conciliar un período
Los listados aceptan ?from= y ?to= en ISO-8601 y paginan por cursor, y
GET /v1/recharges/statement resume el período de una cuenta. Las mismas tres cosas valen
para GET /v1/service-payments, con los códigos de su propia familia.
Sin from/to y sin seguir el cursor, solo ves tus últimas
limit recargas (tope 500). Todo lo anterior a esa página es inalcanzable
hasta que pagines: manda ?cursor= con el next_cursor de la respuesta
(con o sin fechas) mientras has_more sea true, o acota con
from/to un rango que sepas que cabe en una sola página. Un integrador
que solo llama una vez sin fechas y sin mirar has_more puede creer que vio "todas"
sus recargas cuando en realidad vio solo las 500 más recientes.
El rango es semiabierto: [from, to). Incluye from y excluye
to, así que enero y febrero nunca comparten la venta del instante del corte y sumar los
doce meses da el año exacto. Sin zona horaria las fechas se leen en UTC. Si escribes el
desplazamiento (+00:00), escápalo o usa la forma Z: en una query
string el + se decodifica como espacio y la petición se rechaza con
recharge.invalid_period.
curl -s "https://api.winal.com.mx/v1/recharges?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z&limit=500" \
-H "Authorization: Bearer sk_test_..."
# La respuesta trae has_more y next_cursor; repite mientras has_more sea true:
curl -s "https://api.winal.com.mx/v1/recharges?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z&limit=500&cursor=<next_cursor>" \
-H "Authorization: Bearer sk_test_..."
El cursor es opaco: reenvíalo tal cual y no lo construyas a mano. Ordena por (fecha, id), que
es un orden total, así que un lote de recargas enviado en el mismo milisegundo no se salta ni se
repite entre páginas. Un cursor corrupto responde 400 recharge.invalid_cursor en vez de
devolverte la primera página otra vez —una continuación silenciosa te haría sumar dos veces las mismas
ventas—.
Antes de sumar un período, separa las filas por outcome (ver
El desenlace): solo delivered y failed están
settled: true. Una fila con outcome: "undetermined" no es un fracaso —
pero cubre DOS situaciones distintas, y el estado de cuenta las distingue con
in_flight.charged_by_provider_count y su importe
in_flight.charged_by_provider_cost_minor —que es la única cifra de este bloque que
cruza contra el estado de cuenta de tu agregador, porque cost_minor incluye también
las pending, que el proveedor nunca vio—: status: "pending" todavía no se
pidió al proveedor y no le costó saldo a la bolsa; status: "processing" ya se pidió
y puede haber llegado al teléfono sin que Winal lo sepa todavía (regla 5:
processing solo se resuelve con evidencia del proveedor, nunca por tiempo). Si
excluyes una undetermined de tu corte de hoy, vuelve a consultarla en unos días —
GET /v1/recharges/{id} con el mismo id — antes de darla por perdida:
tratarla como failed antes de tiempo es exactamente el error que le haría creer a
alguien que falta dinero cuando en realidad ya se entregó.
El campo con el que se concilia es provider_cost_minor
Cada recarga trae ahora provider_cost_minor (lo que el agregador descontó de verdad
de la bolsa), client_reference (el rastro que viaja con la transacción y vuelve en el
reporte de ventas del proveedor), provider_transaction_id, connector_key,
livemode y requested_at.
provider_cost_minor puede venir en null, y hay que preverlo. El
proveedor no siempre lo reporta —una operación resuelta por conciliación contra su reporte de ventas
puede llegar sin él, y una sin desenlace nunca lo tiene—. Cuando falta, Winal usa el costo del
catálogo (face_amount_minor − merchant_commission_minor), que es lo que se
esperaba cobrar y no necesariamente lo que se cobró. Por eso el estado de cuenta publica
without_provider_cost: una diferencia contra el estado de cuenta del agregador del
tamaño de unos pocos cargos puede venir de ahí y no de un faltante.
El estado de cuenta
curl -s "https://api.winal.com.mx/v1/recharges/statement?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer sk_test_..." \
-H "Winal-Account-Key: sk_test_de_la_farmacia"
Los bloques que devuelve, y qué significa cada uno:
| Bloque | Qué es |
|---|---|
period | Las fechas efectivas (eco de lo pedido) y el livemode reportado. |
charges | Lo ENTREGADO: conteo, suma nominal, suma del costo real, comisión y without_provider_cost. |
in_flight | Lo que al cerrar el período seguía sin desenlace. charged_by_provider_count y charged_by_provider_cost_minor dicen cuántas ya le costaron saldo al agregador y por cuánto (las pending no cuentan: es el importe que cruza contra su estado de cuenta). cost_minor suma todo el bloque, pendientes incluidas. |
failed | Conteo de lo que el proveedor rechazó. No consume saldo: se publica para que no lo busques. |
service_payments | Pagos de servicio del período. Van aparte de las recargas porque su tabla no guarda el costo del proveedor ni la comisión, así que se concilian con menos precisión; el ambiente sí lo guardan, y se acotan por él (livemode_scoped: true). |
deposits | Los abonos que recibió la cuenta en el período (declarado, acreditado y comisión). Se ancla en la fecha PROPIA del abono (cuándo se depositó), no en la de ninguna recarga — un abono de julio sigue en el estado de cuenta de julio aunque el saldo que soltó se gaste en agosto. Siempre viaja: en modo prueba llega con available: false y el motivo, como bag.Desde 0152 trae además attributed_count / attributed_claimed_minor —cuántos de esos abonos entraron SOLOS, reconocidos por la referencia de depósito de la cuenta— y manual_count, la resta ya hecha: los que alguien tuvo que identificar a mano. Es la única cifra que dice si la referencia por cliente está funcionando: con la referencia bien puesta tiende a cero. |
allowance | El cupo actual de la cuenta. Es un contador corriente, no una foto del período. |
bag | El saldo de la bolsa, solo si la cuenta es su dueña. Si no, viene available: false con el motivo. |
Una cuenta hija no tiene saldo propio, y el estado de cuenta no se lo inventa. El dinero vive
en el agregador a nombre de quien fondeó la bolsa —Winal no custodia fondos—, y una bolsa la
comparten todas las cuentas que su dueño apuntó a ella. Decirle a una farmacia «tu saldo es X» sería
atribuirle el de sus hermanas. Lo suyo son sus movimientos (charges,
in_flight, deposits) y su cupo (allowance).
El CSV
GET /v1/recharges/statement.csv devuelve el mismo período con una línea por
movimiento —recargas y pagos de servicio, distinguidos por la columna tipo— con
costo_proveedor_minor y costo_estimado para que una hoja de cálculo pueda
sumar una columna sin fórmulas. Los pagos de servicio dejan vacías las nueve columnas que su
tabla no tiene —producto, comision_comercio_minor,
costo_proveedor_minor, costo_estimado, txn_proveedor,
referencia_cliente, conector, pedido_id e
intento_no—; el livemode sí lo llevan, para que una línea suelta se pueda
leer sola. costo_estimado importa: en las filas de recarga siempre trae
true/false y nunca va vacío, así que un parser que asuma eso para
todas las filas se rompe justo en las de pago de servicio.
Cada línea de recarga es un intento contra un agregador —el costo real es del intento—, así
que un pedido que Winal surtió por respaldo aparece en DOS líneas: la del intento rechazado (sin
costo) y la del que entregó. Las dos últimas columnas, pedido_id e
intento_no, dicen a qué pedido pertenece cada línea; pedido_id es el
id que te devolvió POST /v1/recharges y el que resuelve
GET /v1/recharges/{id}. Van al final para que ninguna columna que ya leías cambie de
posición.
Cuadrar por agregador: dos bolsas, dos portales
Cuando Winal surte tus recargas por más de un agregador, el fin de mes son DOS cuadres: cada bolsa
contra el portal de su agregador. El estado de cuenta lo trae ya partido: by_connector[]
es el mismo período por agregador, con sus charges, in_flight,
failed, deposits y allowance, y cada cifra del total es la suma
de la misma cifra en los agregadores — al centavo, y una prueba lo cruza en cada build.
El costo real es del intento, y una venta servida por respaldo aparece en el agregador que
ENTREGÓ. Un pedido que TAECEL rechazó y el segundo agregador entregó cuenta como una
failed de TAECEL (sin costo: no consumió saldo) y una entregada del segundo, con el
provider_cost_minor que reportó el segundo — porque es de su bolsa de donde salió el
dinero y contra su portal contra el que cuadra. Atribuirla al primero cuadraría contra el portal
equivocado. El connector_key del pedido dice quién entregó (o, sin entrega, quién fue el
último en intentarlo).
Para ver UN agregador —y solo ése— manda ?connector_key= en el estado de cuenta, en el CSV y
en el listado:
curl -s "https://api.winal.com.mx/v1/recharges/statement?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z&connector_key=taecel" \
-H "Authorization: Bearer sk_live_..."
curl -s "https://api.winal.com.mx/v1/recharges/statement.csv?from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z&connector_key=taecel" \
-H "Authorization: Bearer sk_live_..." -o taecel_agosto.csv
- Con filtro, todo el documento es de ese agregador:
charges,in_flight,failed,deposits(los abonos a SU bolsa),allowance(tu cupo CON ese agregador: un vínculo es por agregador) ybag(el cuadre de SU bolsa, si es tuya). La respuesta lo repite enconnector_key; sin filtro ese campo no aparece y el desglose va enby_connector. - El CSV acotado lleva solo líneas de recarga de ese agregador. Los pagos de servicio no guardan
quién los atendió, así que no se pueden atribuir: no van en el archivo acotado (y sí en el completo). El
bloque
service_paymentsdel JSON tampoco se acota, y sunotelo dice. - En el listado, un pedido es de un agregador si ese agregador lo entregó o, sin entrega, fue el
último en intentarlo — la misma definición de
connector_keyen cada recarga. - Una clave que Winal no conoce se rechaza con
400 recharge.connector_key_unknownen vez de devolverte ceros: un estado de cuenta vacío con200te haría creer que ese agregador no vendió nada. El mensaje lista los conocidos.
Dos ventanas, y no es un descuido. charges y by_connector usan el
[from, to) de la API; el bloque bag cuadra entre las DOS LECTURAS de saldo que
acotan tu período y cuenta la venta del instante exacto de la lectura que cierra ((desde, hasta]),
porque esa lectura ya la refleja. Si una venta cae justo en to, la verás en la bolsa de este
período y en los cargos del siguiente; no falta ni sobra dinero.
Conciliar un depósito
Si lo que quieres es saber si ya entró el depósito que una farmacia acaba de reportar, el camino
corto es el webhook recharge.deposit.detected
(Winal lo manda la primera vez que ve el abono) o POST /v1/recharge-balance/refresh para leer
el saldo al momento — contrato de las dos en
Referencia de API → Depósitos. Lo que sigue es la conciliación con
detalle, movimiento por movimiento.
Cuando depositas saldo directo en tu cuenta del agregador (Winal no lo custodia — ver
Los dos techos del dinero), la forma de confirmar que entró era mirar el saldo
antes y después y hacer la resta. GET /v1/recharges/bag-movements te ahorra la resta:
lee el HISTORIAL de movimientos que reporta el propio agregador, incluidos los ABONOS
(depósitos), para que cruces tu comprobante —fecha, importe, folio— contra un movimiento concreto en
vez de inferir que "algo entró".
curl -s "https://api.winal.com.mx/v1/recharges/bag-movements?from=2026-08-20T00:00:00Z" \
-H "Authorization: Bearer sk_live_..."
Cada movimiento trae is_deposit (derivado de que el proveedor lo marque como
"Abono"), amount_minor interpretado y raw_amount —el importe
EXACTO tal como lo escribió el proveedor, sin interpretar—, para que la primera vez que cuadres un
depósito puedas comparar contra el crudo y no solo contra un número ya convertido.
pouch_name puede no ser "Tiempo Aire" ni "Servicios": el mismo reporte ve TODAS las
bolsas del agregador (timbres CFDI, SMS…), así que un abono de otra bolsa también aparece aquí.
Solo ves tu propia bolsa, nunca la de quien te administra. A diferencia del saldo
(/balance, que sí es visible a quien consume) y del estado de cuenta
(/statement, acotado a tus propios movimientos), el historial CRUDO de una bolsa
compartida mezcla el consumo de TODAS las cuentas que la apuntan. Si tu cuenta consume la bolsa de
tu integrador, esta ruta te devuelve una página vacía: sigue viendo lo tuyo en
/statement (bloque deposits).
Es una caché sincronizada, no una llamada en vivo. Un ciclo periódico lee "hoy" y "ayer" del
agregador cada ~15 minutos; synced_through dice hasta cuándo se sabe con certeza que
no faltan filas, y coverage_from desde cuándo hay historial (los movimientos de ANTES
de esa fecha nunca se leyeron — no es que no existieran). Si acabas de depositar, puede tardar hasta
el siguiente ciclo en aparecer.
Y hay un tercer estado que conviene mirar antes de concluir nada: si
truncated_at trae fecha, ese día se leyó a medias — la lista del agregador
venía paginada y se alcanzó el tope de páginas. El listado está INCOMPLETO aunque
synced_through tenga valor, así que no encontrar un depósito ahí no prueba que no
exista. El siguiente ciclo lo reintenta; si persiste, avísanos.
Configurar tus bolsas de saldo
Desde tu tablero, sección Recargas: registra una o varias
bolsas con tu llave y NIP de TAECEL —una por farmacia, una por grupo, o una sola para
todo tu negocio, el mismo mecanismo sirve para los tres casos—, apunta tu cuenta a la que va
a usar, activa las operadoras que quieras vender, y arma ahí mismo el comando para probar
una recarga con tu propia llave sk_. A diferencia del resto de tu integración,
registrar una bolsa y decidir de cuál consume una cuenta son actos que exigen confirmar tu
contraseña y tu segundo factor (mismo criterio que cargar un CSD): con esas credenciales se
gasta saldo real e irreversible, así que una cookie robada no puede recorrer ese camino.
El tablero además te muestra el saldo real de cada bolsa —Winal lo consulta contra
TAECEL al guardar la credencial y lo sigue vigilando después— sin que tengas que entrar al
portal de TAECEL para verlo.
Ahí mismo ves a dónde depositar (botón «A dónde depositar» en cada bolsa): las cuentas
bancarias de TAECEL (CLABE, número de cuenta y, si aplica, tarjeta) y la referencia de depósito de
cada bolsa, sin salir a su portal a buscarlas. Es dato sensible —coordenadas bancarias reales, aunque
no un secreto tuyo— y por eso viaja completo solo aquí, en tu tablero con sesión, nunca por una
llave sk_: así una credencial de API filtrada no puede leerlo. A diferencia de registrar
una bolsa o decidir de cuál consume una cuenta, verla no pide el paso extra de contraseña y
segundo factor: es una lectura y no decide a dónde se mueve un peso, así que basta con la sesión
abierta (con el mismo rol que sea, incluido «solo lectura»). La referencia por bolsa la publica
TAECEL como propia de cada una, pero no está confirmado que un depósito se concilie solo por
traerla — trátala como una ayuda para tu ficha de depósito, no como una garantía.
Traspasar saldo entre tus bolsas
Si administras varias cuentas y cada una tiene su PROPIA cuenta en el agregador —el caso de un
integrador con decenas de farmacias—, fondear a cada una hoy exige un depósito bancario por
separado. POST /v1/recharge-transfers mueve saldo YA depositado de una bolsa tuya a
otra: depositas una vez a tu bolsa central y repartes desde tu sistema, sin volver al banco por
cada una.
No es lo mismo que un depósito a una bolsa compartida (consola: Cuentas de clientes →
Asignar depósito, ver Los dos techos del dinero). Ese depósito acredita
cupo: el dinero ya estaba en una única bolsa y solo cambia a quién le toca. Un traspaso mueve
saldo de VERDAD entre DOS cuentas distintas del agregador. Son los dos modelos de reparto y no se
mezclan: si la cuenta destino consume la MISMA bolsa desde la que traspasarías, esta ruta lo
rechaza (recharge_transfer.same_bag) y te dirige al depósito.
requesting y processing NO son un fracaso: son el estado de un
traspaso del que todavía no hay evidencia concluyente, y el saldo pudo haberse movido ya.
No lo vuelvas a ordenar. Consulta GET /v1/recharge-transfers/{id} hasta ver un
estado terminal (settled o failed), o reintenta con la MISMA
Idempotency-Key: nunca crea un traspaso nuevo. Sobre uno ya pedido
(requesting, processing) te devuelve ese mismo y no vuelve a pedir nada;
sobre uno que se quedó pending —reservado, sin haber llegado a pedirse— lo REANUDA, que
es la única cosa que un reintento puede empujar.
pending: qué pasó y cómo se salepending está reservado y no tocó al agregador: no se movió un peso. El
caso normal es que la bolsa origen no tenga las credenciales del agregador capturadas en ese ambiente,
y entonces la respuesta es 400 recharge_transfer.provider_unavailable. Captúralas en
Recargas → Bolsas y reintenta con la MISMA Idempotency-Key: ese reintento
reanuda el mismo traspaso (una sola fila, un solo folio). Esa llave no queda quemada por el error —es
uno de los pocos rechazos que la capa de idempotencia trata como transitorio, precisamente para que la
salida funcione—. Y si nadie reintenta, el traspaso no se queda invisible: el conciliador lo cuenta y
te manda una alerta operativa.
El destino se nombra por CUENTA, nunca por la cuenta en el agregador. Mandas
destination_account con el identificador que te devolvió POST /v1/accounts
—la cuenta que administras—, y Winal resuelve de ahí su bolsa y, de la bolsa, a qué cuenta del
agregador llega el saldo. No existe ningún campo para mandar esa cuenta del agregador directamente:
si lo hubiera, cualquiera podría nombrar la de un desconocido y el dinero saldría igual. Si la cuenta
tiene su cuenta propia en el agregador dada de alta por Winal, ese número
ya está; si su bolsa es tuya, antes tienes que haberlo puesto en Recargas → Bolsas → «Cuenta en el
agregador» de la bolsa destino.
Exige Idempotency-Key (como cualquier escritura de dinero) y el mismo permiso
recharges:write que vender — ver El permiso que exige una
recarga. Y es de tu propia cuenta, no delegable: no admite Winal-Account-Key
—ordenar un traspaso es mover tu propia tesorería, no operar la cuenta de un cliente— y si la mandas
responde 403 account.delegation_not_allowed. Tiene además su PROPIO tope de actividad
(recharge.transfer, de fábrica 20 por hora y $2,000,000.00 MXN por hora —
el único acto de esta página con techo de monto de fábrica, porque un traspaso siempre declara
cuánto mueve): ver Topes: el freno que tú ajustas.
Contrato completo —cuerpo, campos de la respuesta y los códigos de error— en Referencia de API → Traspasos de saldo entre bolsas.