Vende tiempo aire y paquetes de datos de las operadoras mexicanas (Telcel, AT&T, Movistar, Unefon, Bait) con un flujo de 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. Es un módulo distinto de Pago de servicios (CFE, agua, telefonía, TV): ahí pagas un adeudo existente contra una referencia; aquí entregas saldo nuevo a un número celular.
La API, el catálogo, las comisiones, la idempotencia y los asientos contables son
reales y definitivos: puedes integrar contra ellos y tu código no cambiará. Pero la
entrega la atiende un gateway simulado de fase 0 (sin red), así que una
recarga responde delivered con un folio SIMRCG-…
sin que ningún saldo llegue al celular.
Para vender recargas de verdad falta contratar un agregador real de recargas y
conectarlo por el puerto IRechargeGateway. No expongas este flujo a
clientes finales hasta entonces: cobrarías por un saldo que no se entrega.
Integra y prueba ahora; enciende la venta cuando el agregador esté conectado.
Esto no se queda en una advertencia: el sistema lo impide por código. Con una
API key sk_live_ (modo producción), POST /v1/recharges se
rechaza antes de persistir o asentar nada con
400 recharge.livemode_unsupported — así es imposible cobrarle a un
cliente por un saldo que nunca llega al celular. Integra y prueba siempre con una
llave sk_test_; el endpoint seguirá bloqueado en producción hasta que
conectes el agregador real.
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": "att", "name": "AT&T", "active": true },
{ "object": "recharge_carrier", "code": "bait", "name": "Bait", "active": true },
{ "object": "recharge_carrier", "code": "movistar", "name": "Movistar", "active": true },
{ "object": "recharge_carrier", "code": "telcel", "name": "Telcel", "active": true },
{ "object": "recharge_carrier", "code": "unefon", "name": "Unefon", "active": true }
]
}
Es un catálogo global (sin variación por tenant), igual que el de billers: agregar una operadora nueva es trabajo de Winal, no algo que el comercio configure. Que una operadora aparezca aquí no significa que ya puedas vender sus productos — para eso hay que activarla primero (paso 2).
2. Activa una operadora para tu comercio
POST /v1/recharge-carriers/{code}/enable es un opt-in por tenant:
activar una operadora es lo que puebla el catálogo de productos de tu comercio (paso
3) — sin activarla, sus productos no aparecen en GET /v1/recharge-products y
vender contra ellos falla con recharge.carrier_not_enabled.
curl -s https://api.winal.com.mx/v1/recharge-carriers/telcel/enable \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{}'
{ "object": "recharge_carrier", "code": "telcel", "name": "Telcel", "active": true }
El cuerpo admite un { "active": false } opcional para desactivar una operadora
que ya habías activado (por default, active es true). Este
endpoint no exige Idempotency-Key: es una operación de configuración, no una
que mueva dinero.
3. Productos (montos) disponibles
GET /v1/recharge-products devuelve el catálogo ya filtrado a las
operadoras que activaste en el paso 2.
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
}
]
}
| 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) o data (paquete con vigencia). |
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).
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", "phone_number": "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",
"created_at": "2026-08-03T04:11:44.083258+00:00",
"updated_at": "2026-08-03T04:11:44.12255+00:00"
}
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.
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).
?limit= es opcional (default 100, máx. 500).
Estados de una recharge
| Estado | Qué significa |
|---|---|
pending | Reservada (Idempotency-Key y montos ya persistidos) pero aún sin desenlace de la operadora — transitorio. |
delivered | La operadora confirmó la entrega del tiempo aire; provider_ref trae su folio. |
failed | La operadora rechazó la recarga — ver failure_reason en la respuesta. |
Un fallo de la operadora no deja el cobro en limbo: si la entrega expira sin
confirmarse (timeout de red hacia el agregador), la recarga se queda en
pending — nunca se marca failed por un timeout local (regla de
arquitectura: processing solo se resuelve con evidencia del proveedor).
Reintentar con la misma Idempotency-Key jamás vuelve a ejecutar la
entrega: si sigue pendiente, Winal consulta el estado real ante la operadora por esa misma
solicitud y resuelve la recarga con esa evidencia — nunca reenvía el celular ni recarga dos
veces. Para el detalle completo de estados y transiciones, ver la
Referencia de API.
Idempotencia
Repetir la misma Idempotency-Key con el mismo product_code,
phone_number y payment_intent_id devuelve la misma recarga (sin
volver a ejecutar la entrega). Con datos distintos bajo la misma llave,
409 recharge.idempotency_conflict.
Contabilidad: sin custodia, suma cero
Winal nunca retiene el dinero de una recarga (ADR-0001, sin custodia): al confirmarse la entrega, el valor nominal cobrado al pagador se reparte por partida doble entre lo que se le debe a la operadora 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_payable:telcel | −48.50 (abono) | Costo mayorista que tu comercio le adeuda a Telcel. |
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 asiento se hace con los montos
congelados al momento de la venta (nunca releyendo el catálogo vivo), así que si el
precio del producto cambia después, el ledger siempre cuadra contra lo que realmente se
cobró. 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.
Errores de recharge-carriers / recharge-products / recharges
| 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_ (modo producción) y el único gateway conectado hoy solo simula la entrega — conecta un agregador real o usa una llave sk_test_ mientras tanto. |
400 | recharge.missing_fields | Falta product_code o 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 | phone_number no son 10 dígitos. |
400 | recharge.carrier_not_enabled | La operadora del producto no está activada para tu tenant — actívala primero (paso 2). |
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. |
409 | recharge.retry_conflict | Ya hay una ejecución en curso para esa misma recarga; reintenta en unos segundos. |
404 | recharge.not_found | El id no existe (o es de otro tenant). |
{
"error": {
"type": "invalid_request_error",
"code": "recharge.missing_fields",
"message": "Se requieren 'product_code' y 'phone_number'.",
"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 gateway actual solo simula la entrega. Usa una llave de prueba (sk_test_) o conecta el agregador.",
"doc_url": "...",
"request_id": "..."
}
}
Endpoints
| Método | Ruta | Notas |
|---|---|---|
| GET | /v1/recharge-carriers | Catálogo global de operadoras. |
| POST | /v1/recharge-carriers/{code}/enable | Opt-in por tenant; no exige Idempotency-Key. |
| GET | /v1/recharge-products | Solo productos de operadoras activadas por tu tenant. |
| POST | /v1/recharges | Requiere Idempotency-Key. |
| GET | /v1/recharges | ?limit= opcional (default 100, máx. 500). |
| GET | /v1/recharges/{id} | 404 recharge.not_found si no existe. |