Analítica de negocio de solo lectura bajo tu API key normal (/v1/reports/*)
— nada de portal admin, nada que mute estado. Todos los montos en centavos, todos los
períodos semi-abiertos [from, to) en ISO-8601.
Resumen del período
GET /v1/reports/summary?from=&to=¤cy= — el número que abre un tablero:
cobrado, ticket promedio, reembolsado, disputas y tasa de aprobación de un período. Todos los
/v1/reports/* de esta sección comparten el mismo par from/to
(ISO-8601, opcionales, default últimos 30 días) salvo donde se indique lo contrario, y el
rango es semi-abierto [from, to).
curl -s "https://api.winal.com.mx/v1/reports/summary?from=2026-07-01T00:00:00Z&to=2026-08-01T00:00:00Z" \
-H "Authorization: Bearer $SK"
{
"object": "reporting.summary",
"currency": "MXN",
"charged_minor": 1598624,
"charge_count": 28,
"average_ticket_minor": 57094,
"refunded_minor": 15000,
"dispute_count": 1,
"disputes_held_minor": 25000,
"approval_rate": 0.933,
"period_from": "2026-07-01T00:00:00Z",
"period_to": "2026-08-01T00:00:00Z"
}
disputes_held_minor es el holdback de las disputas creadas dentro del
período (created_at entre from y to) que siguen con
reserva viva (needs_response o under_review) al momento de la consulta —
no es "cualquier disputa con reserva viva": una abierta ANTES de from y todavía en
revisión no entra en este número, aunque su holdback siga retenido hoy. ?currency= es
opcional (default MXN); un código ISO 4217 inválido responde
400 reports.invalid_currency, y un from/to inválido o
invertido responde 400 reports.invalid_period.
Serie diaria
GET /v1/reports/daily?from=&to=¤cy= — el mismo período que
summary, desglosado día por día: útil para graficar cobrado/reembolsado y detectar
un día atípico sin descargar el CSV completo del ledger.
curl -s "https://api.winal.com.mx/v1/reports/daily?from=2026-07-01T00:00:00Z&to=2026-07-04T00:00:00Z" \
-H "Authorization: Bearer $SK"
{ "object": "list", "data": [
{ "object": "reporting.daily_point", "date": "2026-07-01", "charged_minor": 458200, "refunded_minor": 0, "operation_count": 6 },
{ "object": "reporting.daily_point", "date": "2026-07-02", "charged_minor": 312000, "refunded_minor": 15000, "operation_count": 5 },
{ "object": "reporting.daily_point", "date": "2026-07-03", "charged_minor": 0, "refunded_minor": 0, "operation_count": 0 }
] }
Un día sin operaciones aparece igual, en ceros — no se omite del arreglo.
Desglose por método y por conector
GET /v1/reports/by-method?from=&to=¤cy= y
GET /v1/reports/by-connector?from=&to=¤cy= — el mismo período agrupado
por method (card, spei, dimo, oxxo,
card_present…) o por connector_key ganador del ruteo, cada uno con su
approval_rate — la métrica que responde «¿qué método/conector me está aprobando
peor?» sin cruzar payments a mano.
/v1/reports/by-method, respuesta real{ "object": "list", "data": [
{ "object": "reporting.method_breakdown", "method": "card", "amount_minor": 1256490, "charge_count": 20, "approval_rate": 0.9 },
{ "object": "reporting.method_breakdown", "method": "spei", "amount_minor": 342134, "charge_count": 8, "approval_rate": 1.0 }
] }
/v1/reports/by-connector, respuesta real{ "object": "list", "data": [
{ "object": "reporting.connector_breakdown", "connector_key": "sim", "amount_minor": 1598624, "charge_count": 28, "approval_rate": 0.933 }
] }
Operaciones recientes (paginado)
GET /v1/reports/payments?limit=&cursor= — a diferencia de los cuatro anteriores,
no toma from/to: es un listado paginado por cursor opaco
(más recientes primero), pensado para hojear operaciones una por una en vez de agregarlas. Ver
Referencia → Paginación para el contrato general del
estilo has_more/next_cursor.
curl -s "https://api.winal.com.mx/v1/reports/payments?limit=20" \
-H "Authorization: Bearer $SK"
{
"object": "list",
"data": [
{ "object": "reporting.payment", "id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
"amount_minor": 55000, "currency": "MXN", "method": "card", "connector_key": "sim",
"status": "succeeded", "created_at": "2026-07-08T06:47:46.86Z" }
],
"has_more": true,
"next_cursor": "eyJpZCI6MTI4LCJ0cyI6..."
}
?limit= por omisión 20, tope 100 (se recorta, no falla).
method/connector_key pueden llegar null en un intent que
todavía no se confirmó. Un cursor corrupto o de otra fuente responde
400 reports.invalid_cursor — pasa siempre el next_cursor tal cual, sin
construirlo a mano.
Export del ledger (CSV contable)
GET /v1/reports/ledger.csv?from=&to= — el asiento contable crudo del período,
línea por línea del doble-partida (sin agregar): fecha efectiva, tipo de origen, cuenta y el
cargo/abono en centavos, ya separados en dos columnas en vez de un solo monto con signo. Es el
insumo para cruzar contra tu propia contabilidad cuando las pólizas no
alcanzan el detalle que necesitas.
curl -s "https://api.winal.com.mx/v1/reports/ledger.csv?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z" \
-H "Authorization: Bearer $SK" -o ledger.csv
fecha_efectiva,tipo,cuenta,cargo_minor,abono_minor,referencia
2026-07-06T18:03:11.0000000+00:00,charge,merchant_settlement,150000,0,charge:attempt_852d...
2026-07-06T18:03:11.0000000+00:00,charge,provider_receivable:sim,0,150000,charge:attempt_852d...
Streaming directo al cuerpo de la respuesta (no se buferea el período completo en memoria);
Content-Disposition: attachment; filename="ledger_{from}_{to}.csv". No admite
?currency=: trae TODAS las monedas del período tal como están en el ledger.
from/to inválido o invertido → 400 reports.invalid_period.
Retenciones (régimen de plataformas)
GET /v1/reports/retenciones?period=YYYY-MM&format= — el informe de retenciones
de ISR/IVA del régimen de plataformas digitales, por vendedor (sub-comercio de
Winal Connect): la base que se cruza para el DIOT / declaración
informativa (LIVA 18-J-III). A diferencia del resto de /v1/reports/*, el período es
mensual (?period=, formato YYYY-MM) y obligatorio —
from/to no aplican aquí.
curl -s "https://api.winal.com.mx/v1/reports/retenciones?period=2026-07" \
-H "Authorization: Bearer $SK"
{
"object": "retention_report",
"period": "2026-07",
"vendor_count": 2,
"total_gross_minor": 458200,
"total_base_minor": 394655,
"total_isr_retenido_minor": 4582,
"total_iva_retenido_minor": 39466,
"total_retenido_minor": 44048,
"data": [
{ "connect_account_id": "b1e2...", "sub_merchant_name": "Farmacia del Centro",
"rfc": "EWE1709045U0", "clabe_masked": "•••• 4321", "person_type": "moral",
"operation_count": 12, "gross_minor": 458200, "base_minor": 394655,
"isr_retenido_minor": 4582, "iva_retenido_minor": 39466, "total_retenido_minor": 44048,
"currency": "MXN" }
]
}
Con ?format=csv, la misma información llega como
text/csv; charset=utf-8 con
Content-Disposition: attachment; filename="retenciones_{period}.csv" — columnas
vendedor,rfc,clabe,tipo_persona,operaciones,monto_operacion_minor,base_minor,
isr_retenido_minor,iva_retenido_minor,total_retenido_minor,moneda. Sin
?period=, o con un formato distinto de YYYY-MM:
400 reports.invalid_period.
Propinas y corte de caja
La propina viaja como tip_minor (centavos), separada del consumo
(amount_minor). El caso típico de POS: se omite al crear el intent y se fija
hasta confirm, cuando la terminal pregunta "¿propina?".
curl -s https://api.winal.com.mx/v1/payment_intents/5b6b8b3e-.../confirm \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "payment_token": "tok_sim_ok", "payment_method": "card", "tip_minor": 5000 }'
{
"id": "a55fc12f-0af3-4858-ba84-52a3e7240f52",
"object": "payment_intent",
"amount_minor": 50000,
"tip_minor": 5000,
"total_minor": 55000,
"currency": "MXN",
"status": "processing",
"...": "..."
}
total_minor = amount_minor + tip_minor — es el monto que de verdad se captura
con el proveedor (la propina es dinero, no metadata). Si mandas tip_minor en
confirm, reemplaza la propina que el intent ya tuviera (p. ej. la que
pusiste al crear); si lo omites, la propina existente no se toca. Nunca negativo
(payment_intent.invalid_tip, ver Errores).
GET /v1/reports/cash-cut?from=&to=
El corte de caja del turno: desglose por método y por conector (cobrado, propinas, número de
operaciones), totales generales y devoluciones del período. A diferencia del resto de
/v1/reports/*, aquí from y to son
obligatorios — fase 0 no tiene una entidad de "turno"; lo defines tú con la hora de
apertura/cierre de caja.
curl -s "https://api.winal.com.mx/v1/reports/cash-cut?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z" \
-H "Authorization: Bearer $SK"
{
"object": "reporting.cash_cut",
"currency": "MXN",
"period_from": "2026-07-01T00:00:00Z",
"period_to": "2026-07-08T00:00:00Z",
"by_method": [
{ "key": "card", "charged_minor": 256490, "tip_minor": 6500, "total_minor": 262990, "operation_count": 13 },
{ "key": "dimo", "charged_minor": 25000, "tip_minor": 0, "total_minor": 25000, "operation_count": 1 }
],
"by_connector": [
{ "key": "sim", "charged_minor": 281490, "tip_minor": 6500, "total_minor": 287990, "operation_count": 14 }
],
"totals": { "charged_minor": 281490, "tip_minor": 6500, "total_minor": 287990, "operation_count": 14 },
"refunds": { "count": 0, "amount_minor": 0 }
}
Sin from/to válidos (o con from ≥ to), 400 reports.invalid_period. ?currency= es opcional (default MXN).
Multisucursal: filtra el corte por sucursal/caja
No hay campos tipados de sucursal/caja en el payment_intent — es
metadata libre: manda metadata.branch y/o
metadata.register al crear el intent (los códigos que quieras, sin necesidad de
registrarlos antes en ningún catálogo):
curl -s https://api.winal.com.mx/v1/payment_intents \
-H "Authorization: Bearer $SK" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": 800,
"currency": "MXN",
"metadata": { "branch": "centro", "register": "caja1" }
}'
El catálogo de sucursales/cajas (nombres legibles, activar/desactivar) se administra en
portal → Sucursales — es puramente informativo para tu propia organización:
payment_intents acepta cualquier valor de metadata.branch/
metadata.register exista o no en ese catálogo, así que no necesitas darla de alta
antes de empezar a cobrar con ella.
GET /v1/reports/cash-cut suma dos filtros opcionales de igualdad exacta,
branch= y register=:
curl -s "https://api.winal.com.mx/v1/reports/cash-cut?from=2026-07-01T00:00:00Z&to=2026-07-09T00:00:00Z&branch=centro" \
-H "Authorization: Bearer $SK"
{
"object": "reporting.cash_cut",
"currency": "MXN",
"period_from": "2026-07-01T00:00:00+00:00",
"period_to": "2026-07-09T00:00:00+00:00",
"by_method": [
{ "key": "card", "charged_minor": 800, "tip_minor": 0, "total_minor": 800, "operation_count": 1 }
],
"by_connector": [
{ "key": "sim", "charged_minor": 800, "tip_minor": 0, "total_minor": 800, "operation_count": 1 }
],
"totals": { "charged_minor": 800, "tip_minor": 0, "total_minor": 800, "operation_count": 1 },
"refunds": { "count": 0, "amount_minor": 0 }
}
Sin filtro de branch=, la respuesta suma un campo extra by_branch
—el mismo desglose de siempre (key, charged_minor,
tip_minor, total_minor, operation_count), agrupado por
metadata.branch. Los cobros sin sucursal capturada se agrupan bajo la llave
literal "(sin sucursal)":
{
"object": "reporting.cash_cut",
"...": "...",
"by_branch": [
{ "key": "(sin sucursal)", "charged_minor": 1589124, "tip_minor": 9500, "total_minor": 1598624, "operation_count": 27 },
{ "key": "centro", "charged_minor": 800, "tip_minor": 0, "total_minor": 800, "operation_count": 1 }
]
}
by_branch solo se omite si filtras por branch=register= (sin branch=) sigue trayendo
by_branch completo — el campo desaparece de la respuesta (nunca llega como
null) únicamente cuando la consulta ya fijó una sucursal específica, porque en
ese caso el desglose por sucursal es redundante con el filtro que ya aplicaste.
Informe de ahorro
GET /v1/reports/savings?from=&to= (ambos opcionales; default: últimos 30
días) — cuánto le ahorró/recuperó Winal al tenant, por fuente:
| Campo | Qué significa |
|---|---|
routing_saved_minor | Ahorro por rutear al conector más barato elegible en vez del más caro (ruteo consciente de costo). |
retry_recovered_minor | Monto recuperado por reintentar cross-conector un soft decline que de otra forma se habría perdido. |
dunning_recovered_minor | Monto recuperado por el dunning de suscripciones (reintentos automáticos de un cobro fallido, 1d/3d/5d). |
total_minor | Suma de las tres fuentes. |
by_connector | El mismo desglose, por conector ganador. |
curl -s "https://api.winal.com.mx/v1/reports/savings" -H "Authorization: Bearer $SK"
{
"object": "reporting.savings",
"routing_saved_minor": 0,
"retry_recovered_minor": 0,
"dunning_recovered_minor": 0,
"total_minor": 0,
"by_connector": [],
"period_from": "2026-06-07T23:02:03Z",
"period_to": "2026-07-07T23:02:03Z"
}
En cero es una respuesta válida: un tenant sin ruteo por costo activado, sin soft declines
reintentados ni suscripciones en dunning durante el período simplemente no generó ahorro que
atribuir. Activa route_by_cost en el portal para empezar a ver
routing_saved_minor.
Exports contables (pólizas)
GET /v1/reports/polizas?from=&to=&format=contpaqi|aspel_coi — pólizas
contables del período, listas para importar en CONTPAQi o Aspel-COI. A diferencia del resto
de /v1/reports/*, format es obligatorio (no hay un formato
"correcto" por default).
curl -s "https://api.winal.com.mx/v1/reports/polizas?from=2026-07-01T00:00:00Z&to=2026-07-08T00:00:00Z&format=contpaqi" \
-H "Authorization: Bearer $SK" -o polizas.txt
P 20260706 3 1 1 0 Poliza diario Winal 2026-07-06
M 108-001 1 0 150.00 0 0.00 charge: charge:attempt_852d...
M 102-001 2 1 150.00 0 0.00 charge: charge:attempt_852d...
Dr,06/07/2026,Poliza diario Winal 2026-07-06
108-001,charge: charge:attempt_852d...,150.00,0.00
102-001,charge: charge:attempt_852d...,0.00,150.00
La respuesta llega como text/plain con
Content-Disposition: attachment; filename="polizas_{formato}_{from}_{to}.txt"
— una póliza por día del período. Si algún código contable de Winal no tiene un override
propio configurado en el portal, el export usa un mapeo por default y lo avisa en el header
X-Winal-Account-Mapping-Defaults (lista de codes separados por coma) para que
sepas cuáles revisar con tu contador. format inválido u omitido →
400 reports.invalid_format.