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.

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 fromto), 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.