Autenticación y seguridad

GUÍA

Toda petición autenticada a Winal viaja por TLS y presenta tu llave secreta como un Bearer token. Esta página consolida en un solo lugar cómo se autentica cada superficie del API, cómo se rotan y expiran las llaves, y la postura de red y de acceso al portal.

Autenticación de la API

Winal expone tres superficies con esquemas de autenticación distintos. El securityScheme del contrato OpenAPI declara exactamente el de /v1.

SuperficieCómo se autenticaQuién la llama
/v1/*Authorization: Bearer sk_test_… / sk_live_…tu backend (nunca el navegador)
/public/*el client_secret del intent (en el cuerpo o el query); sin cabecera Authorizationel navegador del pagador vía winal.js
/health, /status, /pay/*, /webhooks/in/*sin autenticaciónmonitoreo, checkout hosteado, callbacks del proveedor
http
GET /v1/payment_intents/5b6b8b3e-... HTTP/1.1
Host: api.winal.com.mx
Authorization: Bearer sk_test_9fZ3kQ...
🔑 La llave secreta es secreta
sk_ = secret key. Va solo en tu servidor. No hay una "llave publicable" que exponer al navegador: el frente se autentica con el client_secret efímero de un intent, que solo puede confirmar ese cobro y nada más. Si una sk_ se filtra, revócala de inmediato (abajo).

Una petición a /v1 sin Authorization, con una llave mal formada o revocada, responde 401 con el envelope de error estándar (error.type = "authentication_error"; ver Errores).

Ciclo de vida de las llaves

Cada llave se almacena hasheada (jamás en claro): el valor completo se muestra una única vez, al crearla o rotarla. Después solo queda un prefijo enmascarado para identificarla — ni siquiera nosotros podemos recuperarla, así que guárdala al momento. La rotación y la revocación son autoservicio: las haces tú mismo desde tu tablero en https://winal.com.mx/app/, sin pedírnoslas.

Rotación sin downtime

Rotar no corta el servicio: al rotar, Winal emite una llave nueva y mantiene la anterior válida durante un periodo de gracia configurable. Despliegas la nueva, verificas que todo tu tráfico ya la usa, y entonces cierras la vieja (o dejas que expire sola al terminar la gracia). Este solape es la forma correcta de cambiar una credencial en producción.

1

Rota

Rotas tú desde tu tablero en https://winal.com.mx/app/; Winal emite la sk_… nueva y fija old_expires_at (fin de la gracia) a la anterior.

→
2

Despliega

Actualizas el secreto en tu backend. Ambas llaves autentican durante la gracia.

→
3

Cierra

Completas la rotación (o esperas a old_expires_at): la vieja deja de servir.

Expiración

Una llave puede tener expires_at. En cuanto pasa esa fecha —o si la revocas— deja de autenticar y toda petición con ella recibe 401. En el listado de tus llaves, el campo active refleja no revocada Y (sin expiración O aún no expirada).

Revocación inmediata

🔓 La revocación es self-service
Desde tu tablero en https://winal.com.mx/app/ puedes cortar cualquiera de tus llaves tú mismo (DELETE /app/api/api-keys/{id}), sin escribirnos ni esperar a nadie. Tenlo en tu runbook de incidentes: si se te filtra una llave (un commit accidental, un log, un ticket), entra al tablero y revócala de inmediato.

Si una llave se compromete, revócala tú mismo desde el tablero: el corte es inmediato, sin gracia. Emite una llave nueva y actualiza tu backend. La revocación queda en la bitácora de auditoría del tenant (api_key.rotated / api_key.created y la revocación), con actor y fecha.

⚠️ Nunca en el cliente ni en el repo
Mantén sk_live_ fuera de HTML, apps móviles, repos y logs. Si tu stack lo permite, inyéctala como variable de entorno o desde un gestor de secretos. Un secreto en un commit se considera comprometido aunque borres el commit después — rótalo.

Scopes de la API key

Además de identificar al tenant, una API key lleva una lista de scopes: permisos granulares que acotan QUÉ puede hacer esa llave, más allá de a quién pertenece. Los eliges tú al emitirla, en tu tablero (winal.com.mx/app → Llaves de API → Crear llave): así una llave filtrada solo puede hacer lo que marcaste, y puedes tener varias con permisos distintos — la del punto de venta no tiene por qué poder dispersar dinero ni cerrar tu contabilidad. Sin elegir nada, la llave nace con los permisos de operación, que es lo que necesita una integración de cobro y facturación.

Emitir —o rotar— una llave de PRODUCCIÓN, o una con un permiso reservado al dueño (payouts:write, recharges:write), pide tu contraseña y tu segundo factor, igual que cargar un sello digital. Rotar cuenta como emitir: la sucesora nace con el mismo ambiente y los mismos permisos, y su valor completo se muestra ahí. Una sk_test_ de operación no lo pide: es la higiene diaria de integrar, y pedir la contraseña en cada una solo entrena a teclearla.

Cambiar los permisos de una llave que ya existe

Los scopes no quedan fijos al emitir. En tu tablero, Llaves de API → Editar permisos sobre cualquier llave viva te deja marcar o quitar permisos de la MISMA credencial: el cambio surte efecto de inmediato y no tienes que generar una llave nueva ni redistribuirla en tu sistema. Es la salida a 403 insufficient_scope con una llave que ya está desplegada en producción. Sigue las mismas reglas que emitir: añadir cualquier permiso pide contraseña y segundo factor (el servidor decide si aplica, según lo que la llave gane); quitar permisos no pide nada, por lo mismo que apretar cualquier otro control no lo pide — es la reacción de emergencia.

ScopeExigido por
payments:readToda lectura de /v1 (GET). Es el permiso mínimo: una llave sin él no puede consultar nada.
payments:writeToda escritura de /v1 que no caiga en un permiso más específico de esta tabla: crear y confirmar cobros, links de pago, suscripciones, clientes de facturación, y emitir CFDI (POST /v1/invoices*). Es lo que usa un punto de venta.
refunds:writePOST /v1/refunds — devolver un cobro. Va aparte porque es salida de dinero.
customers:writeEscrituras de /v1/customers y /v1/payment_methods — dar de alta clientes y guardar sus métodos de pago.
disputes:writeEscrituras de /v1/disputes — responder un contracargo con evidencia.
webhooks:managePOST/DELETE /v1/webhook_endpoints — alta/baja de a dónde se entregan tus eventos.
reports:writePOST /v1/reports/periods/{year}/{month}/close — cierre irreversible de un período contable. Se pide aparte porque no tiene reapertura.
payouts:writeEscrituras de /v1/payouts, /v1/connect/transfers y /v1/payroll — toda operación que ORDENA una salida real de dinero por SPEI, y los datos de nómina de los que sale. Solo el dueño de la cuenta puede emitir una llave con él.
accounts:writePOST /v1/accounts — dar de alta las cuentas de tus propios clientes. Aparece en tu tablero cuando Winal habilita tu cuenta para administrar cuentas (ver Cuentas de tus clientes); además exige una ventana de alta abierta.
*Comodín de acceso total: satisface cualquier scope. No se emite por autoservicio —sería saltarse de un plumazo las condiciones de arriba— y pedirlo responde api_key.invalid_scope.

Una llave sin el scope requerido recibe 403 con error.type = "authorization_error" y error.code = "insufficient_scope" (ver Errores) — el mensaje nombra exactamente el scope que falta.

La regla es que NO hay ruta de /v1 sin permiso exigido: leer pide payments:read, escribir pide payments:write, y los dominios de la tabla piden el suyo. Eso es lo que hace que el techo de una llave filtrada sea el que marcaste al emitirla — antes, cinco de estos permisos no los exigía ninguna ruta y una llave marcada solo como «Consultar cobros» podía cobrar, reembolsar y timbrar CFDI. Si añadimos un endpoint nuevo, nace exigiendo el permiso general de su método: no depende de que alguien se acuerde de gatearlo.

En una operación DELEGADA sobre una cuenta que administras, los permisos efectivos son la intersección de los de las dos credenciales: ni tu llave gana poder por prestarse la de tu cliente, ni al revés. Ver Cuentas de tus clientes.

Idempotencia como salvaguarda

Todo POST que mueve dinero exige un header Idempotency-Key (un UUID que tú generas). Reintentar con la misma clave devuelve la respuesta original sin duplicar el cargo — tu red de seguridad ante timeouts y reintentos. Es una de las tres capas de idempotencia de Winal (API, hacia el proveedor, y dedupe por event_id en tus consumidores de webhook). Detalle en Referencia de API.

Límite de solicitudes

El API aplica rate limiting por ventana fija de 1 minuto. Las peticiones autenticadas se cuentan por llave; las de /public/* por IP de cliente. Al excederlo recibes 429 con Retry-After: 60. Los detalles y los límites por defecto están en Referencia → Límites de solicitudes.

Postura de red

Orígenes permitidos

Puedes declarar desde qué direcciones IP y rangos CIDR (IPv4 e IPv6) valen las credenciales de tu cuenta. Es una lista, no una sola dirección, porque cualquier operación seria tiene segunda instancia y ambiente de pruebas. Se administra desde tu tablero (winal.com.mx/app → Seguridad → Orígenes permitidos), y el cambio es un reemplazo atómico de la lista completa: mudarte de región es una sola operación que o queda o no queda, sin un intermedio en el que te dejes fuera a ti mismo.

Firma de peticiones (llave privada tuya)

Una API key es un secreto compartido: viaja entera en cada petición, así que existe a la vez en tu memoria, en tu configuración, en cualquier proxy que termine TLS y en cualquier herramienta que loguee cabeceras. La firma ataca esa familia entera de fugas: tú conservas tu llave privada y nos registras solo la pública. Tu API key sigue diciendo quién eres; la firma prueba que eres tú, y un volcado completo de nuestra base no permitiría suplantarte.

Registras tu llave desde el tablero (Seguridad → Llaves de firma) como bloque PEM PUBLIC KEY o como certificado CERTIFICATE — RSA de 2048 bits o más, o ECDSA P-256 o mayor. No somos una autoridad certificadora: aceptamos un autofirmado, porque lo que ancla la confianza es que la subiste con tu sesión (contraseña y segundo factor), no la firma de una CA. Si la subes dentro de un certificado, respetamos su vigencia.

El formato exacto de la cabecera Winal-Signature, la cadena que se firma renglón por renglón y cada error posible con su causa están en Errores → Origen acotado y firma de peticiones. Si además administras cuentas, hay un script de openssl ya verificado —incluida la cabecera Winal-Account-Key dentro de la firma— en Cuentas administradas → Firma de peticiones.

Las dos formas conviven, y se migra sin romper nada
Toda cuenta empieza con la exigencia de firma apagada: si ya integras con API key, no tienes que hacer nada. Con la exigencia apagada, una petición sin firma se atiende igual que siempre y una con firma se verifica de verdad — así pruebas tu implementación sin ventanas de silencio, porque una firma rota nunca pasa inadvertida. Cuando funcione, enciendes la exigencia desde tu tablero. Y el camino de vuelta está abierto: si pierdes tu llave privada registras otra o apagas la exigencia, y si te la roban puedes revocarla al instante aunque sea la única — tu API queda cerrada, que es justo lo que quieres ante un robo, y se reabre registrando otra.
mTLS
El mTLS mutuo en el borde sigue en el roadmap y se habilita por acuerdo. Para la mayoría de los casos que lo pedían, la firma de peticiones más los orígenes permitidos cubren lo mismo sin depender de la configuración del proxy.

Tu tablero

Desde https://winal.com.mx/app/ tu equipo administra la cuenta sin escribirnos — llaves, facturación, conectores, ruteo y quién tiene acceso (detalle completo en Tu tablero). Dos piezas de seguridad viven ahí:

Si tu equipo entra con SSO

Una identidad federada no tiene contraseña ni segundo factor locales —los tiene en tu proveedor de identidad—, así que no inscribe 2FA en Winal (portal_auth.sso_managed_totp): dejarlo sería permitir que la propia sesión se acuñara el factor con el que después se confirma, y eso no es un segundo factor sino la cookie otra vez. Para los actos peligrosos, Winal pide en su lugar que tu IdP vuelva a autenticar a la persona:

Desde la consola no tienes que hacer nada especial. Cuando una acción exige confirmación, aparece el botón «Continuar con mi proveedor»: te lleva con tu IdP, te autenticas, vuelves a la consola y repites la acción. Ni contraseñas ni tokens que copiar a mano.

Por API, la ceremonia son dos llamadas —y tiene que ser en ese orden, porque lo que ata la confirmación a UNA operación se acuña en la primera:

  1. POST /app/api/security/reauth/start con method y path del acto que vas a ejecutar. Devuelve un state, un nonce y la URL de autorización de tu IdP con prompt=login y max_age=0.
  2. POST /app/api/security/reauth/complete con ese state y el id_token que devolvió el IdP —que debe traer el nonce del reto—. Devuelve un boleto.

El boleto va en la cabecera Winal-Reauth-Assertion al repetir el acto. Vale para ese acto (el verbo y la ruta quedaron escritos en el reto), para esa persona, una sola vez y durante cinco minutos; y la aserción tiene que traer un auth_time reciente, que es lo único que distingue «mi IdP me acaba de autenticar» de «mi IdP me emitió un token desde una sesión de hace tres días». Si tu conexión no declara el authorization_endpoint, /start devuelve el reto igual, sin URL: la consola te enseña el nonce para que le pidas la aserción a tu IdP por tu cuenta. Los errores están en la tabla de errores, con la familia portal_auth.reauth_* y …step_up_reauth_*.

Acceso al portal de plataforma

Distinto de tu tablero está el portal de plataforma (/portal): la consola interna que usa el equipo de Winal para operar TODOS los comercios. Es multi-usuario, con roles por usuario y sesiones por cookie igual que tu tablero, pero no está expuesto a internet —se opera por túnel administrativo, y por eso /portal responde 404 desde fuera—. Estas son sus protecciones:

Las llaves de API, la sesión de tu tablero y el acceso al portal de plataforma son planos de seguridad separados: cerrar una sesión no invalida las sk_, y revocar una sk_ no cierra ninguna sesión de portal. Gestiona cada uno según su riesgo.