Cobra CFE, agua, telefonía, TV y recargas con el mismo flujo de tres pasos: consulta el catálogo, consulta el adeudo de una referencia (sin mover dinero), y confirma el pago del servicio. Útil para un POS que quiere ofrecer "pago de servicios" en el mismo mostrador donde ya cobra con tarjeta o SPEI.
1. Catálogo de billers
curl -s https://api.winal.com.mx/v1/billers \
-H "Authorization: Bearer $SK"
{
"object": "list",
"data": [
{ "object": "biller", "code": "agua_cdmx", "name": "Sistema de Aguas de la Ciudad de México (SACMEX)", "category": "agua", "reference_label": "Cuenta de agua (8 a 10 dígitos)", "active": true },
{ "object": "biller", "code": "cfe", "name": "CFE (Comisión Federal de Electricidad)", "category": "luz", "reference_label": "Número de servicio (10 a 12 dígitos)", "active": true },
{ "object": "biller", "code": "telcel_recarga", "name": "Recarga Telcel", "category": "recarga", "reference_label": "Número celular a 10 dígitos", "active": true },
{ "object": "biller", "code": "telmex", "name": "Telmex", "category": "telefonia", "reference_label": "Número telefónico o de contrato (10 dígitos)", "active": true },
{ "object": "biller", "code": "izzi", "name": "izzi Telecom", "category": "tv", "reference_label": "Número de cuenta izzi (10 a 12 dígitos)", "active": true }
]
}
Filtra con ?category= (luz, agua, gas,
telefonia, tv, recarga, gobierno) — hoy el
catálogo de Sim solo sembró los cinco de arriba; gas y gobierno son
categorías válidas del enum sin ningún biller activo todavía. Es un catálogo global (sin
variación por tenant): agregar un biller nuevo es trabajo de Winal, no algo que el comercio
configure.
2. Consulta de adeudo (inquiry)
POST /v1/billers/{code}/inquiry no mueve dinero — solo pregunta cuánto debe una
referencia. Úsalo para mostrarle el monto al pagador antes de cobrarle.
Fase 0 corre inquiry contra SimBillerGateway, no contra CFE/agua/Telmex/izzi
de verdad: amount_due_minor sale de un hash determinista (FNV-1a) de tu
reference, no de una consulta al prestador del servicio. La MISMA referencia siempre
da el MISMO monto — útil para pruebas repetibles — pero ese monto no corresponde a ningún
recibo real.
Este endpoint está además bloqueado en producción (sk_live_): el gateway
declara SupportsLivemode: false y toda la ruta de pago de servicios lo rechaza antes
de cobrar nada. Si tu integración muestra amount_due_minor en pantalla sin dejar claro
que es de PRUEBA, un cajero puede cobrarle al cliente una cifra inventada creyendo que es el
adeudo real del recibo.
Qué usar en su lugar: el pago de servicios REAL (luz, agua, telefonía, TV de paga) se
cobra por el camino de recargas, con un producto de monto
libre: POST /v1/recharges contra un prestador conectado, mandando
amount_minor con el importe del recibo.
inquiry/service-payments con sk_test_ siguen sirviendo
para desarrollar y probar el flujo completo — nunca para operar un mostrador real.
Y ese importe ya NO tienes que sacarlo del papel que trae el cliente. Esta página decía
que «Winal no puede consultarlo por ti»; era cierto y dejó de serlo. La consulta del adeudo
REAL vive en POST /v1/service-debt-inquiries
(documentada aquí): le pregunta al prestador cuánto debe esa
referencia, con el mismo agregador y las mismas credenciales con las que después vas a cobrar.
La admiten 8 de las 172 operadoras del catálogo —CFE entre ellas—, y cuáles exactamente lo dice
supports_balance_inquiry en GET /v1/recharge-carriers.
curl -s https://api.winal.com.mx/v1/billers/cfe/inquiry \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{ "reference": "1234567890" }'
{
"object": "biller_inquiry",
"biller_code": "cfe",
"biller_name": "CFE (Comisión Federal de Electricidad)",
"reference": "1234567890",
"amount_due_minor": 118700,
"currency": "MXN",
"service_holder_name": "Cliente simulado (ref. 1234567890)",
"due_date": "2026-07-18T06:47:41.852307+00:00"
}
reference debe cumplir el formato que declara reference_label del biller
(p. ej. 10-12 dígitos para cfe). En Sim, el adeudo es determinista por referencia
(la misma referencia siempre da el mismo amount_due_minor), lo que te deja escribir
pruebas repetibles.
3. Pago del servicio
POST /v1/service-payments confirma el pago ante el biller. Exige
Idempotency-Key (UUID) — este endpoint valida el header él mismo, con el mismo mensaje
que el resto de la API. Si omites amount_minor, se cobra el adeudo vigente tal cual; si
lo mandas, debe coincidir exactamente con el adeudo (fase 0 no admite pagos parciales).
curl -s https://api.winal.com.mx/v1/service-payments \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "biller_code": "cfe", "reference": "1234567890" }'
{
"id": "3f490a8e-630e-4a4b-ba4e-81be735c0c14",
"object": "service_payment",
"biller_code": "cfe",
"reference": "1234567890",
"amount_minor": 118700,
"currency": "MXN",
"status": "paid",
"provider_ref": "SIMBILL-7188545cf02c4532b9681371fbe806b3",
"created_at": "2026-07-08T06:47:46.857935+00:00",
"updated_at": "2026-07-08T06:47:46.863537+00:00"
}
Si ya cobraste al pagador con un payment_intent propio (p. ej. le cobraste por tarjeta
en tu POS y ahora pagas el servicio con ese dinero), manda payment_intent_id para
enlazar ambos registros — es un campo de correlación libre, sin validar contra Payments (Billers no
referencia otros módulos).
201 con
status: "failed" y failure_reason con el detalle, mismo criterio que un
cargo declinado por el emisor en Payments.
{ "id": "...", "object": "service_payment", "status": "failed",
"failure_reason": "El biller 'Telmex' rechazó la confirmación del pago para la referencia '...' (simulado)." }
Estados de un service_payment
| Estado | Qué significa |
|---|---|
pending | Registrado, aún no confirmado ante el biller (transitorio). |
paid | El biller confirmó el pago del servicio. |
failed | El biller rechazó la confirmación — ver failure_reason. |
Idempotencia
Repetir la misma Idempotency-Key con el mismo cuerpo devuelve la misma fila (mismo
201, sin volver a confirmar ante el biller). Con un cuerpo distinto bajo la misma
llave, 409 service_payment.idempotency_conflict.
Errores de billers / service-payments
| HTTP | code | Causa |
|---|---|---|
400 | biller.missing_reference | Falta reference en el inquiry. |
400 | biller.invalid_code | El code/biller_code llega vacío. |
404 | biller.not_found | code no existe o está inactivo. |
400 | biller.invalid_reference | reference no cumple el formato del biller (ver reference_label). |
404 | biller.reference_not_found | Formato válido pero la cuenta no existe para ese biller. |
400 | biller.livemode_unsupported | Consultaste el adeudo (inquiry) con una llave sk_live_ y el proveedor conectado solo simula. |
400 | service_payment.missing_fields | Falta biller_code o reference. |
400 | idempotency_key_required | Falta el header o no es un UUID válido. |
409 | service_payment.idempotency_conflict | Misma llave, cuerpo distinto. |
400 | service_payment.amount_mismatch | amount_minor enviado no coincide con el adeudo vigente. |
400 | service_payment.livemode_unsupported | Pagaste con una llave sk_live_ y el proveedor conectado solo simula. |
404 | service_payment.not_found | El id no existe (o es de otro tenant). |
404 | service_payment.payment_intent_not_found | El payment_intent_id con el que quieres ligar el pago no existe. |
409 | service_payment.retry_conflict | Ya hay un reintento de ese mismo pago en curso: consúltalo en vez de mandar otro. |
400 | service_payment.invalid_period | El from/to del listado 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 usar Z. |
400 | service_payment.invalid_cursor | El cursor no es uno emitido por esta API. Usa tal cual el next_cursor de la página anterior. |
Un agregador real es negocio regulado
Todo lo de arriba corre hoy contra el simulador de billers (adeudos deterministas, sin llamada de
red real). Conectar un agregador real de pago de servicios es, en México, actividad regulada:
cobrar y dispersar fondos de terceros por esta vía puede requerir estructurarse a través de un
agregador ya autorizado para no romper "sin custodia de fondos" (ADR-0001). Es una decisión de
producto/legal pendiente del dueño, no una limitación técnica del código — el puerto que reemplaza
al simulador (IBillerGateway) ya está listo para recibir un adaptador real sin tocar
nada de lo documentado arriba.
Endpoints
| Método | Ruta | Notas |
|---|---|---|
| GET | /v1/billers | ?category= opcional. |
| POST | /v1/billers/{code}/inquiry | No mueve dinero; no exige Idempotency-Key. ⚠️ Adeudo SIMULADO, no real — ver arriba. |
| POST | /v1/service-payments | Requiere Idempotency-Key. |
| GET | /v1/service-payments | ?limit= opcional (default 100, máx. 500), ?from=/?to= ISO-8601 y ?cursor=. Devuelve has_more y next_cursor. El rango es semiabierto [from, to), igual que en recargas y en los reportes. |
| GET | /v1/service-payments/{id} | 404 service_payment.not_found si no existe. |
Un pago de servicio se concilia con menos precisión que una recarga, y conviene saberlo antes de
cuadrar un mes. Su tabla guarda el importe cobrado y poco más: no registra el costo que
el proveedor descontó, ni una comisión del comercio, ni el agregador que lo atendió. El
ambiente sí lo registra, así que el estado de cuenta los acota igual que a las recargas
(livemode_scoped: true); aun así GET /v1/recharges/statement los publica
en un bloque aparte, en vez de sumarlos con las recargas:
mezclar dos cifras de distinta exactitud produce una tercera que no es ninguna de las dos. Lo que sí
viaja es confirm_requested_at, el instante en que se pidió la confirmación al biller
—el análogo del requested_at de una recarga—, que es lo que distingue «espera» de
«nunca se llegó a pedir».