Recargas de tiempo aire

MODO PRUEBA

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.

⚠️ Hoy solo en MODO PRUEBA — el saldo NO llega a ningún teléfono

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

bash
curl -s https://api.winal.com.mx/v1/recharge-carriers \
  -H "Authorization: Bearer $SK"
200 · respuesta real
{
  "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.

bash
curl -s https://api.winal.com.mx/v1/recharge-carriers/telcel/enable \
  -H "Authorization: Bearer $SK" \
  -H "Content-Type: application/json" \
  -d '{}'
200 · respuesta real
{ "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.

bash
curl -s https://api.winal.com.mx/v1/recharge-products \
  -H "Authorization: Bearer $SK"
200 · respuesta real (extracto)
{
  "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
    }
  ]
}
CampoQué es
face_amount_minorValor nominal de la recarga, en centavos: lo que recibe el celular del cliente y lo que le cobras al pagador.
merchant_commission_minorComisión que tu comercio se queda por vender esa recarga, en centavos — ya incluida dentro de face_amount_minor, no se suma aparte.
kindairtime (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.

bash
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" }'
201 · respuesta real
{
  "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"
}
El celular SIEMPRE se devuelve enmascarado
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

bash
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

EstadoQué significa
pendingReservada (Idempotency-Key y montos ya persistidos) pero aún sin desenlace de la operadora — transitorio.
deliveredLa operadora confirmó la entrega del tiempo aire; provider_ref trae su folio.
failedLa 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:

CuentaMovimientoQué 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

HTTPcodeCausa
400recharge.invalid_carrierFalta el código de operadora en /enable.
404recharge.carrier_not_foundEl código de operadora no existe o está inactivo.
400recharge.livemode_unsupportedLa 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.
400recharge.missing_fieldsFalta product_code o phone_number.
400idempotency_key_requiredFalta el header Idempotency-Key o no es un UUID válido.
404recharge.product_not_foundproduct_code no existe o no está activo.
400recharge.invalid_phonephone_number no son 10 dígitos.
400recharge.carrier_not_enabledLa operadora del producto no está activada para tu tenant — actívala primero (paso 2).
404recharge.payment_intent_not_foundpayment_intent_id no existe o no pertenece a tu tenant.
400recharge.payment_intent_not_succeededEl payment_intent enlazado no está en succeeded.
400recharge.payment_intent_amount_mismatchEl monto del payment_intent no coincide con el nominal de la recarga.
409recharge.idempotency_conflictMisma llave, datos distintos.
409recharge.retry_conflictYa hay una ejecución en curso para esa misma recarga; reintenta en unos segundos.
404recharge.not_foundEl id no existe (o es de otro tenant).
400 · ejemplo real
{
  "error": {
    "type": "invalid_request_error",
    "code": "recharge.missing_fields",
    "message": "Se requieren 'product_code' y 'phone_number'.",
    "doc_url": "...",
    "request_id": "..."
  }
}
400 · ejemplo real (llave sk_live_)
{
  "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étodoRutaNotas
GET/v1/recharge-carriersCatálogo global de operadoras.
POST/v1/recharge-carriers/{code}/enableOpt-in por tenant; no exige Idempotency-Key.
GET/v1/recharge-productsSolo productos de operadoras activadas por tu tenant.
POST/v1/rechargesRequiere Idempotency-Key.
GET/v1/recharges?limit= opcional (default 100, máx. 500).
GET/v1/recharges/{id}404 recharge.not_found si no existe.