Reportes

MODO PRUEBA

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=&currency= — 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).

bash
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"
200 · respuesta real
{
  "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=&currency= — 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.

bash
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"
200 · respuesta real
{ "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=&currency= y GET /v1/reports/by-connector?from=&to=&currency= — 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.

200 · /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 }
] }
200 · /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.

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

bash
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
200 · text/csv, fragmento real
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í.

bash · JSON
curl -s "https://api.winal.com.mx/v1/reports/retenciones?period=2026-07" \
  -H "Authorization: Bearer $SK"
200 · respuesta real (fragmento)
{
  "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?".

bash · confirm con 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 }'
200 · respuesta real
{
  "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.

bash
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"
200 · respuesta real
{
  "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):

bash
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=:

bash · filtrado por sucursal
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"
200 · respuesta real, filtrada
{
  "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)":

200 · respuesta real, SIN filtro de branch (fragmento)
{
  "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=
Filtrar solo por 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:

CampoQué significa
routing_saved_minorAhorro por rutear al conector más barato elegible en vez del más caro (ruteo consciente de costo).
retry_recovered_minorMonto recuperado por reintentar cross-conector un soft decline que de otra forma se habría perdido.
dunning_recovered_minorMonto recuperado por el dunning de suscripciones (reintentos automáticos de un cobro fallido, 1d/3d/5d).
total_minorSuma de las tres fuentes.
by_connectorEl mismo desglose, por conector ganador.
bash
curl -s "https://api.winal.com.mx/v1/reports/savings" -H "Authorization: Bearer $SK"
200 · respuesta real
{
  "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).

bash · CONTPAQi
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
200 · fragmento real (formato CONTPAQi)
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...
200 · fragmento real (formato Aspel-COI)
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.