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.
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. |
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 | 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. |
404 | service_payment.not_found | El id no existe (o es de otro tenant). |
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. |
| POST | /v1/service-payments | Requiere Idempotency-Key. |
| GET | /v1/service-payments | ?limit= opcional (default 100, máx. 500). |
| GET | /v1/service-payments/{id} | 404 service_payment.not_found si no existe. |