Winal versiona en dos planos que no debes confundir: la ruta
(/v1), que cambia solo ante rediseños incompatibles a gran escala, y la
versión de API por fecha (api_version), que fija la forma exacta de
los payloads. La regla de oro: tu integración no se rompe por un cambio que tú no
pediste.
La ruta /v1
Todos los endpoints de integración cuelgan de /v1. Ese número mayor solo
cambiaría ante un rediseño incompatible del API completo — algo raro y muy anunciado.
Mientras exista /v1, seguirá aceptando el contrato que documentamos aquí.
api_version por fecha
La forma concreta de los recursos se identifica con una versión con fecha, del estilo
2026-07-01. La verás en cada entrega de webhook, en el campo
api_version del envelope:
{
"event_id": "8f2a1c3e-...",
"event_type": "payment_intent.succeeded",
"api_version": "2026-07-01",
"livemode": false,
"data": { "...": "..." }
}
Esa marca te dice contra qué contrato serializamos el payload. Fíjala en tu integración (guárdala o valídala) para saber, sin ambigüedad, qué forma esperar.
Cómo introducimos cambios
Distinguimos cambios aditivos (seguros, entran sin aviso) de cambios rompientes (nunca en silencio). Escribe tu cliente para tolerar los primeros.
| Aditivo (no rompe) | Rompiente (nunca en silencio) |
|---|---|
| Un campo nuevo en una respuesta | Renombrar o quitar un campo existente |
| Un valor nuevo en un enum | Cambiar el tipo de un campo |
| Un endpoint o recurso nuevo | Volver requerido un parámetro antes opcional |
Un event_type nuevo | Cambiar el significado de un campo o de un estado |
| Un header de respuesta nuevo | Retirar un endpoint |
- Ignora campos que no conozcas en vez de fallar al deserializar.
- Trata los enums como abiertos: maneja un
statusoevent_typedesconocido con una rama por defecto, no con un crash. - Recuerda que los campos
nullse omiten en las respuestas de/v1(no llegan comonull, simplemente no están) — no asumas que una llave siempre existe.
Los cambios rompientes, cuando sean inevitables, se introducen bajo una
api_version nueva y con aviso previo; tu integración existente sigue
recibiendo la forma con la que la escribiste hasta que decidas migrar.
Deprecación
Cuando algo va a retirarse, lo marcamos como deprecado en la Referencia y en las notas de cambio, con una alternativa recomendada y una ventana de transición. Un endpoint deprecado sigue funcionando durante esa ventana; no desaparece de un día para otro.
2026-09-24 — Depósitos: el formulario de TAECEL, el aviso y el saldo al momento
Lo que un punto de venta necesita para que una farmacia fondee su saldo y sepa cuándo ya puede vender. Todo es aditivo salvo una corrección, abajo. El comprobante lo recibe y lo valida TAECEL en su propio formulario; Winal no ve ni guarda el archivo.
- Endpoints nuevos:
GET /v1/recharge-report-links(el enlace del formulario de reporte de depósitos de cada bolsa propia de la cuenta, incluida la cuenta madre del integrador) yPOST /v1/recharge-report-links/refresh(volver a leerlo del agregador). - El saldo al momento:
POST /v1/recharge-balance/refreshlee la bolsa en el momento, con un freno de 10 minutos por bolsa;GET /v1/recharges/balancegana el bloquerefresh. - Un
event_typenuevo:recharge.deposit.detected, uno por abono nuevo en una bolsa de la cuenta. Si tu endpoint filtra por lista blanca de eventos, no lo recibirás hasta que lo agregues; si recibe todo, trata el tipo como desconocido o procésalo. - Códigos nuevos:
recharge.balance_refresh_unavailable,recharge.report_link_unavailable,recharge.report_link_source_not_found,recharge.report_link_requires_live_key(pedirlo con llave de prueba llamaría al agregador con credenciales REALES). - Corrección en
GET /v1/recharges/balance:uses_own_credentialssalíafalsepara cualquier bolsa —también la de la cuenta madre del integrador y la cuenta propia de una farmacia—, lo contrario de lo que el campo documenta (false= «ese saldo NO es tuyo, es de quien te administra»). Ahora estruecuando la bolsa es de la cuenta yfalsesolo cuando es de quien la administra. Si tu punto de venta ocultaba el saldo de una bolsa propia por ese campo, a partir de hoy lo verá.
2026-09-20 — Varios agregadores de recargas: respaldo, ruteo y cuadre por agregador
Un solo programa en tres piezas, todas aditivas, en POST /v1/recharges,
GET /v1/recharges, GET /v1/recharges/{id} y
GET /v1/recharges/statement(.csv): nada se renombra, nada cambia de significado, ningún
estado nuevo, ningún campo que ya leías cambia de posición. Si tu integración ya funciona, sigue
funcionando sin tocar nada. Lo que sigue es lo que puedes empezar a usar.
-
Una recarga es un pedido que Winal puede surtir por más de un agregador. Cuando el primero
contesta un rechazo firme, Winal la intenta por el siguiente —en la misma petición si el rechazo fue
inmediato— y te devuelve un solo resultado. Nunca ante un timeout: ahí la transacción pudo entrar y
pasar a otro agregador serían dos recargas al mismo teléfono. Tampoco cuando un intento se cierra por
no aparecer en el reporte de ventas del agregador: ese cierre termina el pedido
(
failure_code: "not_requested") sin respaldo, porque la ausencia en un reporte acotado por tiempo no prueba que no se entregó. Campos nuevos:attempts[](un intento por agregador, con su estado, transacción, costo real y por qué fue ahí),attempt_countyfailure_code(solo constatus: "failed"). Los campos que ya leías —provider_ref,provider_transaction_id,provider_cost_minor,client_reference,connector_key,requested_at— son los del intento que entregó (o del último), que para un pedido de un solo intento son exactamente los de siempre. Elides el del pedido y es el mismo que ya guardaste: todos los ids emitidos antes siguen resolviendo. Detalle en Recargas → Cuando un agregador falla. -
Winal elige el agregador por salud, saldo y prioridad. Cada
attempts[].routingtraeevaluated[]: los agregadores evaluados conprovider_key,eligibley el motivo en español («cortacircuito abierto hasta 21:32 UTC por 3 fallos del agregador para 'telcel' en 10 min», «sin saldo suficiente: $180.00 disponibles para $194.00», «prioridad del comercio para 'telcel': posición 1», «más barato de los sanos: $47.00 frente a $48.00»). Tu prioridad por operadora se fija en el tablero → Recargas → Agregadores, donde también ves por dónde iría AHORA una recarga y por qué. Los filtros, en orden, en Recargas → Cómo elige Winal el agregador. -
Cuadre por agregador.
GET /v1/recharges/statementganaby_connector[]—el mismo período por agregador: cargos, en vuelo, rechazadas, abonos y cupo— y cada cifra del total es la suma de la misma cifra en los agregadores. El estado de cuenta, el CSV y el listado aceptan?connector_key=para ver UN agregador; una clave desconocida se rechaza con400 recharge.connector_key_unknown(nunca ceros). Una venta servida por respaldo aparece en el agregador que entregó, con su costo real. El CSV acotado lleva solo líneas de recarga (los pagos de servicio no guardan agregador); el CSV completo gana dos columnas al final,pedido_ideintento_no. La respuesta repiteconnector_keysolo cuando filtras. Ver Recargas → Cuadrar por agregador.
-
Un cierre que Winal ya no publica como fracaso firme. Cuando un intento no aparece en el
reporte de ventas del agregador, Winal cierra la recarga —no la reintenta por otra vía— pero
no afirma que no se entregó: el reporte se sella con el reloj del agregador y una venta que sí
entró puede caer fuera de la ventana. Ese cierre trae ahora
failure_code: "absent_from_report"(valor NUEVO del catálogo cerrado defailure_code) conoutcome: "undetermined",settled: false,safe_to_retry: falsey el campo nuevoneeds_review: true. Antes salía comonot_requestedconsafe_to_retry: true, y eso te pedía hacer exactamente el doble envío que Winal acababa de no hacer. Si programasif (!safe_to_retry) return;—lo que la doc recomienda— no tienes que cambiar nada: ahora te frena donde antes te dejaba seguir. Y Winal sigue leyendo los reportes de los tres días siguientes: si la venta aparece entregada, esa MISMA recarga pasa adeliveredsola (por esosettledesfalse); si aparece fracasada, pasa afailure_code: "failed"consafe_to_retry: true. Ver Recargas → Lo que no se pudo determinar se vuelve a mirar.
Lo que NO cambia. Nada en la API nombra al agregador salvo el connector_key que ya venía
en cada recarga y el filtro nuevo; el traspaso de saldo sigue exigiendo dos cuentas en el
agregador (el destino es un clienteID del proveedor, no otra bolsa dentro de la misma cuenta);
pouch_id sigue siendo solo del JSON del estado de cuenta; y las reglas de dinero —un timeout no es
un fracaso, una entrega no se reversa— son las mismas.
2026-09-18 — Tu Idempotency-Key ahora es de tu ambiente
Un cambio de contrato en toda operación que mueve dinero o timbra un comprobante, más una capacidad nueva para quien administra cuentas. Cinco minutos de lectura; probablemente no tengas que cambiar nada.
El cambio de contrato: qué es distinto y qué sigue igual
Una Idempotency-Key se identificaba por (tu cuenta, la llave). Ahora se
identifica por (tu cuenta, el AMBIENTE de tu credencial, la llave) — ver el detalle en
Entornos y Base URL. Aplica a toda
ruta que mueve dinero o emite un comprobante fiscal: cobros, refunds, payouts, traspasos de
Connect, facturas, notas de crédito y complementos de pago
(REP), recargas y pagos de servicio, y el nuevo
traspaso de saldo entre bolsas.
Dentro de un mismo ambiente, nada cambia: la misma llave con el mismo cuerpo te sigue
devolviendo la misma operación sin volver a ejecutarla, y con otro cuerpo sigue siendo un
conflicto. Lo que cambia es el cruce ENTRE ambientes. Antes, tu sk_test_ y tu
sk_live_ compartían el espacio de llaves de tu cuenta; si reutilizabas la misma
Idempotency-Key en los dos —lo típico si la derivas de tu folio de ticket y
probaste en sandbox con los mismos folios con los que vendes de verdad—, tu primera operación
real en producción podía recibir de vuelta, silenciosamente, la respuesta que ya se había
cacheado en pruebas: un acuse de éxito por una operación que solo ocurrió en el simulador.
Ahora esa misma llave desde el otro ambiente es una petición nueva, que ejecuta y crea
su propia operación.
Qué tienes que hacer: nada, si ya generas llaves distintas por ambiente (lo más común). Si derivas tu llave de un folio propio y ese folio se repite entre tu integración de pruebas y tu operación real, ya lo sabes: tu primera venta real con un folio que ya usaste en sandbox ahora se procesa de verdad, en vez de devolverte el resultado de la prueba.
Esto se arregló, y pudo haberte tocado
-
Reintentar una factura (
POST /v1/invoices/{id}/retry) y las dos rutas de cancelación ahora comparan el ambiente contra el del comprobante mismo (un dato de su propia fila, fijado al timbrar y que nunca cambia), no solo contra tu perfil fiscal actual. Un CFDI que nació en pruebas se reintenta y se cancela SIEMPRE consk_test_y el perfil ensandbox— aunque tu cuenta ya esté en producción. Detalle eninvoice.livemode_mismatch. -
¿Tienes facturas de PRUEBA que quedaron en
errorde cuando integrabas? No tienes nada que cerrar: nunca tuvieron valor fiscal ni te obligan ante el SAT (y, por lo de arriba, ya ni siquiera se pueden reintentar con tu llave de producción). Factura la venta real como un comprobante nuevo. - Tu factura GLOBAL mensual o bimestral de pruebas ya no ocupa el período de la real. Antes, timbrar la global de agosto en sandbox mientras probabas bloqueaba la global REAL de agosto —una obligación fiscal— y la única salida era mover tu perfil a sandbox, cancelar ahí y regresarlo. Ya no hace falta: las dos globales del mismo mes, una por ambiente, coexisten sin chocar (tampoco se pueden sustituir entre sí). Ver Facturación CFDI → Factura global.
Nuevo: mueve saldo entre tus propias cuentas del agregador
Si administras cuentas de terceros (Cuentas
administradas) y cada una tiene su PROPIA cuenta en el agregador de recargas, hoy
fondear cada una exige un depósito bancario por separado.
POST /v1/recharge-transfers mueve saldo YA depositado de una bolsa tuya a otra
del mismo comercio: depositas una vez a tu cuenta central y repartes desde tu sistema. Detalle
completo en Recargas de tiempo aire → Traspasar saldo entre
tus bolsas.
Requisito real, no de trámite: necesitas al menos DOS cuentas de TAECEL dadas
de alta bajo tu comercio. El destino de un traspaso es un clienteID real del
agregador — no una segunda bolsa que Winal invente por ti —, así que si hoy solo tienes UNA
cuenta activa, el endpoint ya está disponible pero no tiene con qué operar hasta que des de
alta una segunda (recharge_transfer.source_not_found o
recharge_transfer.destination_not_found). Preferimos que lo sepas aquí a que lo
descubras con un error sin contexto.
Es irreversible una vez aplicado —el agregador no cancela ni reversa un traspaso exitoso, igual que una recarga entregada—. Si mandas de más, la única salida es que la cuenta destino te lo regrese con otro traspaso.
Nuevo, y ya lo puedes usar sin condiciones: el historial de tu bolsa
GET /v1/recharges/bag-movements lee el historial de movimientos de tu bolsa
—incluidos los depósitos— tal como lo reporta el agregador, para conciliar un
comprobante de depósito sin hacer la resta de saldos a mano. Ver
Recargas de tiempo aire → Conciliar un depósito.
Changelog
Historial de capacidades del API, de lo más reciente a lo más antiguo. Las fechas son de disponibilidad en modo prueba.
| Fecha | Capacidad | Tipo |
|---|---|---|
2026-09-24 | Depósitos de saldo (migración 0156): GET /v1/recharge-report-links y POST /v1/recharge-report-links/refresh (el enlace del formulario de TAECEL donde la farmacia sube su comprobante, de cualquier bolsa propia; solo con llave sk_live_), POST /v1/recharge-balance/refresh (el saldo al momento, con freno de 10 minutos por bolsa; bloque refresh en GET /v1/recharges/balance) y el webhook recharge.deposit.detected — ver el aviso arriba. | Aditivo |
2026-09-24 | uses_own_credentials de GET /v1/recharges/balance es true para una bolsa de la que la cuenta es dueña (antes salía false para cualquier bolsa). | Corrección |
2026-09-23 | La cuenta propia de cada farmacia en el agregador (migración 0155): desde la ficha de cada cliente, Winal le da de alta su cuenta en TAECEL dentro de tu red de distribuidor —su referencia de depósito, su formulario para reportar el depósito y su saldo—. Endpoint nuevo de solo lectura GET /v1/recharge-subaccounts para que tu punto de venta muestre la referencia y el enlace de reporte de cada farmacia. Un traspaso ya puede llegar a la cuenta propia de la farmacia. Códigos nuevos recharge_subaccount.*. En la consola, mode: "provision" sobre una bolsa propia sigue rechazándose con recharge.provisioning_unavailable, ahora con la dirección correcta. | Aditivo |
2026-09-21 | Depósitos por referencia (migración 0152): una bolsa puede acreditar el cupo de cada cliente a partir de la referencia de depósito con la que el agregador etiquetó el abono, en vez de que una persona identifique cada comprobante. Nace en modo sombra —propone y tú confirmas— porque en qué campo del reporte viaja la referencia todavía no está medido. En el API, el estado de cuenta gana en deposits los campos attributed_count, attributed_claimed_minor y manual_count: cuánto de lo que recibiste entró solo. La configuración vive en la consola (Recargas → Depósitos de tus clientes), no en /v1. | Aditivo |
2026-09-20 | Cierre a revisión de una recarga: failure_code: "absent_from_report" (valor nuevo) con outcome: "undetermined", settled: false, safe_to_retry: false y el campo nuevo needs_review, más la relectura de los reportes de los días siguientes — ver el aviso arriba. | Aditivo |
2026-09-20 | Programa varios agregadores de recargas: respaldo entre agregadores (attempts[], attempt_count, failure_code; dos columnas al final del CSV), ruteo por salud, saldo y prioridad (attempts[].routing.evaluated[]; prioridad y vista previa en el tablero) y cuadre por agregador (by_connector[] y ?connector_key= en estado de cuenta, CSV y listado; recharge.connector_key_unknown) — ver el aviso arriba. | Aditivo |
2026-09-18 | La Idempotency-Key se identifica por (cuenta, ambiente, llave) en toda ruta que mueve dinero o timbra — ver el aviso completo arriba. | Corrección |
2026-09-18 | Reintentar y cancelar un comprobante cruza el ambiente contra el comprobante, no solo contra tu perfil; la global de pruebas ya no bloquea la global real del período. | Corrección |
2026-09-18 | POST /v1/recharge-transfers (traspaso de saldo entre tus bolsas) y GET /v1/recharges/bag-movements (historial de movimientos, incluidos depósitos). | Aditivo |
2026-07-08 | Infraestructura de plataforma: Winal Connect (split/marketplace), Payouts/dispersión, Onboarding KYC/KYB, pago de servicios, BNPL, tokenización de red y card-present. | Aditivo |
2026-07-08 | Antifraude explicable, card-on-file / cobro 1-click, cobranza inteligente, conciliación bancaria y multisucursal/multicaja. | Aditivo |
2026-07-07 | Facturación CFDI 4.0 (PUE y PPD/REP), ruteo por costo con informe de ahorro, pólizas CONTPAQi/Aspel, propinas + corte de caja y CLI winal. | Aditivo |
2026-07-07 | Métodos ampliados y más conectores; metadata del intent en PaymentIntentResponse y en data.metadata del webhook. | Aditivo |
2026-07-07 | Disputas/contracargos, Payment Links + checkout hosteado (/pay/{slug}), suscripciones y SDKs oficiales C#/TS/PHP/Python. | Aditivo |
2026-07-01 | api_version base: payment intents, confirmación pública con client_secret, webhooks firmados y envelope de error estándar. | Base |
Fase 0, modo prueba: el API está estable y probado de extremo a extremo; la única marca
de api_version en circulación es 2026-07-01. Este changelog
crecerá con cada versión con fecha que publiquemos.