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.
| Superficie | Cómo se autentica | Quié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 Authorization | el navegador del pagador vía winal.js |
/health, /status, /pay/*, /webhooks/in/* | sin autenticación | monitoreo, checkout hosteado, callbacks del proveedor |
GET /v1/payment_intents/5b6b8b3e-... HTTP/1.1
Host: api.winal.com.mx
Authorization: Bearer sk_test_9fZ3kQ...
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.
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.
Despliega
Actualizas el secreto en tu backend. Ambas llaves autentican durante la gracia.
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
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.
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.
| Scope | Exigido por |
|---|---|
payments:read | Toda lectura de /v1 (GET). Es el permiso mínimo: una llave sin él no puede consultar nada. |
payments:write | Toda 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:write | POST /v1/refunds — devolver un cobro. Va aparte porque es salida de dinero. |
customers:write | Escrituras de /v1/customers y /v1/payment_methods — dar de alta clientes y guardar sus métodos de pago. |
disputes:write | Escrituras de /v1/disputes — responder un contracargo con evidencia. |
webhooks:manage | POST/DELETE /v1/webhook_endpoints — alta/baja de a dónde se entregan tus eventos. |
reports:write | POST /v1/reports/periods/{year}/{month}/close — cierre irreversible de un período contable. Se pide aparte porque no tiene reapertura. |
payouts:write | Escrituras 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:write | POST /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
- TLS obligatorio. Todo el tráfico entra por HTTPS en el borde (Caddy termina TLS con certificados gestionados). Las peticiones en claro se redirigen/rechazan.
- IP de cliente confiable. Winal toma la IP real del último salto que anexa el proxy de confianza, no un
X-Forwarded-Forarbitrario — así el particionado de rate limit y la telemetría no son falsificables desde el cliente. - Sin custodia de fondos. Winal orquesta el cobro pero nunca custodia tu dinero: las CLABEs y credenciales de proveedor son tuyas (ADR-0001). Reduce drásticamente la superficie de un incidente.
- Nunca tocamos el PAN. Ningún endpoint acepta el número de tarjeta; la tokenización es del lado del cliente con los campos seguros del proveedor (PCI SAQ A). Ver Métodos de pago.
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.
- Opcional para un negocio suelto. Si facturas desde tu local no tienes IP fija, y no vamos a exigirte algo que no puedes dar. Sin lista declarada, nada cambia.
- Obligatoria en cuanto administras cuentas de tus clientes (cuentas administradas,
POST /v1/accounts): tu credencial abre las suyas y firma sus comprobantes fiscales, así que no puede valer desde cualquier punto de internet. Se te pide al crear la primera, no después. - Además detecta. Un intento desde una dirección no declarada se rechaza con
403 account.origin_not_allowedy te llega como alerta operativa con la dirección exacta — que es el primer síntoma de una credencial filtrada, y también el dato que necesitas si resulta ser tu servidor nuevo. - La dirección es la real. Sale del último salto que anexa nuestro proxy de confianza, nunca de un
X-Forwarded-Forque mande el cliente: una lista que se salta escribiendo una cabecera no es una lista.
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.
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í:
- 2FA (TOTP). Cada persona de tu equipo activa su propio segundo factor desde la sección Seguridad del tablero, con cualquier app de códigos (Google Authenticator, 1Password, Authy). Con 2FA activo, el login exige el código de 6 dígitos además de la contraseña — y es uno de los requisitos del checklist de producción.
- Recuperación de contraseña. Si la olvidas, "¿Olvidaste tu contraseña?" en el login te manda un enlace de un solo uso; al canjearlo se cierran todas tus sesiones activas, por si el olvido fue en realidad un acceso indebido.
- Equipo con roles. El dueño de la cuenta agrega y deshabilita miembros con rol
owner,operadorolecturadesde el tablero; el rol de lectura no puede escribir nada, ni con la cabecera anti-CSRF correcta. - Confirmación para los actos peligrosos. Crear una identidad, cambiarle el rol o sacar a alguien del equipo piden tu contraseña y tu segundo factor, no solo tener la sesión abierta: son los actos con los que alguien que consiguió tu cookie echaría raíces, y borrar al otro dueño es exactamente igual de grave que degradarlo — quita de en medio a la única persona que podría deshacer el resto. Lo que endurece (revocar una llave, estrechar la lista de orígenes, revocar una invitación pendiente) no pide nada, a propósito: es la reacción de quien acaba de sospechar una fuga y tiene que estar a un clic.
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:
POST /app/api/security/reauth/startconmethodypathdel acto que vas a ejecutar. Devuelve unstate, unnoncey la URL de autorización de tu IdP conprompt=loginymax_age=0.POST /app/api/security/reauth/completecon esestatey elid_tokenque devolvió el IdP —que debe traer elnoncedel 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:
- 2FA (TOTP). Cada usuario puede activar un segundo factor con cualquier app de códigos (Google Authenticator, 1Password, Authy). Con 2FA activo, el login exige el código de 6 dígitos además de la contraseña.
- SSO / OIDC. El portal acepta inicio de sesión con el
id_tokende tu proveedor de identidad (OpenID Connect), para que tu equipo entre con las credenciales corporativas. - Roles. Distingue quién puede ver contra quién puede emitir/revocar llaves y mover configuración sensible.
- Cierre de sesiones. Puedes cerrar todas las sesiones activas de un usuario (revocación global) si sospechas de un acceso indebido.
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.