Pago de servicios (Billers)

MODO PRUEBA

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

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

⚠️ El adeudo que devuelve HOY es INVENTADO, no real

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.

bash
curl -s https://api.winal.com.mx/v1/billers/cfe/inquiry \
  -H "Authorization: Bearer $SK" \
  -H "Content-Type: application/json" \
  -d '{ "reference": "1234567890" }'
200 · respuesta real
{
  "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).

bash
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" }'
201 · respuesta real (pagado)
{
  "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).

El biller puede rechazar la confirmación
Aunque la referencia tenga adeudo válido, el biller puede rechazar el pago al confirmarlo (cuenta cancelada, adeudo ya cubierto por otro medio, etc.). Esto no es un error HTTP — el procedimiento se completó, el rechazo es el desenlace: 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

EstadoQué significa
pendingRegistrado, aún no confirmado ante el biller (transitorio).
paidEl biller confirmó el pago del servicio.
failedEl 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

HTTPcodeCausa
400biller.missing_referenceFalta reference en el inquiry.
400biller.invalid_codeEl code/biller_code llega vacío.
404biller.not_foundcode no existe o está inactivo.
400biller.invalid_referencereference no cumple el formato del biller (ver reference_label).
404biller.reference_not_foundFormato válido pero la cuenta no existe para ese biller.
400biller.livemode_unsupportedConsultaste el adeudo (inquiry) con una llave sk_live_ y el proveedor conectado solo simula.
400service_payment.missing_fieldsFalta biller_code o reference.
400idempotency_key_requiredFalta el header o no es un UUID válido.
409service_payment.idempotency_conflictMisma llave, cuerpo distinto.
400service_payment.amount_mismatchamount_minor enviado no coincide con el adeudo vigente.
400service_payment.livemode_unsupportedPagaste con una llave sk_live_ y el proveedor conectado solo simula.
404service_payment.not_foundEl id no existe (o es de otro tenant).
404service_payment.payment_intent_not_foundEl payment_intent_id con el que quieres ligar el pago no existe.
409service_payment.retry_conflictYa hay un reintento de ese mismo pago en curso: consúltalo en vez de mandar otro.
400service_payment.invalid_periodEl 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.
400service_payment.invalid_cursorEl 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étodoRutaNotas
GET/v1/billers?category= opcional.
POST/v1/billers/{code}/inquiryNo mueve dinero; no exige Idempotency-Key. ⚠️ Adeudo SIMULADO, no real — ver arriba.
POST/v1/service-paymentsRequiere 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».