Saltar al contenido
← Volver al blog
integracionesseguridad

Error de firma en el webhook de Stripe: causas y solución

Por qué Stripe dice que ninguna firma coincide con el payload y cómo resolverlo: secreto equivocado, cuerpo ya parseado, cabecera perdida o reloj desfasado.

Por Equipo Devonauts6 min de lectura
Read in English →
Primer plano de código HTML en un editor de código sobre una pantalla oscura

La verificación de firma de un webhook de Stripe falla cuando uno de tres datos está mal: el secreto de firma, el cuerpo crudo de la petición o la cabecera Stripe-Signature. La causa más común, con diferencia, es usar el secreto whsec_ equivocado: el del Stripe CLI en producción, por ejemplo, o el de un sandbox en un endpoint live. La segunda es un framework que convierte el JSON en objeto antes de que llegue a tu código, de modo que los bytes que firmó Stripe ya no son los que tú verificas.

En esta guía verás cómo saber cuál de los tres falla, la corrección exacta para Express, Next.js y Supabase Edge Functions, y lo que no debes hacer mientras lo arreglas.

¿Qué significa realmente el error?

El mensaje que suele aparecer es este:

No signatures found matching the expected signature for payload.

Stripe firma cada entrega calculando una firma sobre una marca de tiempo más el cuerpo exacto de la petición, con el secreto de tu endpoint. Tu código repite el cálculo y compara. Si cualquiera de los datos cambia en un solo byte, el resultado no coincide. El error no dice cuál está mal, así que hay que revisarlos uno por uno.

Paso 1: ¿Usas el secreto de firma correcto?

La documentación de Stripe lo señala como la causa más frecuente. Cada endpoint tiene su propio secreto, y hay más endpoints de los que uno cree.

De dónde vienen los eventosDónde está el secreto
Stripe CLI (stripe listen)Se imprime en la terminal al iniciar el comando
Endpoint del sandbox en WorkbenchPágina del endpoint, Reveal secret
Endpoint live en WorkbenchLo mismo, con el Dashboard en modo live

Todos empiezan con whsec_ y parecen intercambiables. No lo son. Para comprobarlo:

  1. Registra en el log los primeros y últimos cuatro caracteres del secreto que tu código carga de verdad (nunca el valor completo).
  2. Compáralos con el secreto del endpoint que está enviando ese evento.
  3. Busca problemas invisibles: un espacio o salto de línea al final, comillas pegadas al valor, o la variable definida en un entorno (preview) y no en otro (producción).
  4. Despliega de nuevo. Muchos hostings solo leen las variables al desplegar.

Si el secreto del endpoint se rotó hace poco, el anterior puede seguir vigente un tiempo: Stripe permite retrasar su vencimiento hasta 24 horas y, durante ese lapso, firma con los dos. Cuando el viejo vence, los servidores que lo siguen usando empiezan a fallar.

Paso 2: ¿Tu framework está modificando el cuerpo?

El cuerpo tiene que ser exactamente el texto que mandó Stripe. Cualquier cosa que lo convierta en objeto y lo vuelva a pasar a texto puede reordenar claves o cambiar espacios, y la firma deja de coincidir.

Express

express.json() parsea todas las peticiones que ve. Registra la ruta del webhook con un parser de cuerpo crudo y antes del middleware JSON global:

app.post('/webhook', express.raw({ type: 'application/json' }), handler);
app.use(express.json()); // resto de las rutas

Next.js con App Router

Lee el cuerpo como texto, no como JSON:

const body = await req.text();
const event = stripe.webhooks.constructEvent(body, signature, secret);

Llamar primero a req.json() y luego hacer JSON.stringify del resultado es una forma muy común de romper la verificación. En el Pages Router, desactiva el body parser integrado para esa ruta de API y lee el buffer crudo.

Supabase Edge Functions (Deno)

En Deno hay dos detalles propios. Usa la versión asíncrona con el proveedor de Web Crypto y lee el cuerpo con req.text():

const cryptoProvider = Stripe.createSubtleCryptoProvider();
const event = await stripe.webhooks.constructEventAsync(
  await req.text(), signature, secret, undefined, cryptoProvider
);

Además, despliega esa función con la verificación de JWT desactivada. Stripe no envía un token de Supabase; si la verificación está activa, Supabase responde 401 antes de que corra tu código. Es otro error, pero muchas veces se reporta como "el webhook no funciona". Lo tratamos en errores de Edge Functions en apps de Lovable.

Los proxies, los API gateways y algunos middleware también pueden reescribir el cuerpo. Si tu app está detrás de uno, asegúrate de que el cuerpo crudo llegue intacto a tu función.

Paso 3: ¿La cabecera de firma llega completa?

Registra el valor de la cabecera (es seguro hacerlo). Debería verse como t=1700000000,v1=…, quizá con más entradas. Si llega vacía, probablemente lees un nombre de cabecera equivocado o algún framework la descarta. Los nombres de cabecera no distinguen mayúsculas, pero algunas utilidades que las convierten en un objeto simple sí lo hacen.

Paso 4: ¿La marca de tiempo es demasiado vieja?

Las librerías de Stripe rechazan eventos cuya marca de tiempo firmada se aleja más de 5 minutos de la hora actual de tu servidor. Eso frena ataques de repetición, pero también atrapa servidores con el reloj mal configurado y manejadores que encolan el cuerpo para verificarlo mucho después. Mantén el reloj sincronizado con NTP y verifica en cuanto llega la petición. No pongas la tolerancia en 0: la documentación de Stripe aclara que así se desactiva por completo el control de antigüedad.

Cada reintento lleva una marca de tiempo y una firma nuevas, así que reenviar un evento viejo desde el Dashboard no tiene este problema.

¿Cómo confirmas que quedó resuelto?

  1. Abre el endpoint en Workbench y entra a la pestaña Event deliveries. Las entregas fallidas muestran el código HTTP que devolvió tu servidor.
  2. Reenvía un evento fallido y espera un 2xx.
  3. En local, ejecuta stripe listen --forward-to localhost:3000/api/webhook y stripe trigger checkout.session.completed, usando el secreto que imprimió el CLI.

Después procesa los eventos que se perdieron mientras estuvo roto. En modo live, Stripe reintenta las entregas fallidas durante un máximo de tres días, así que las más recientes pueden recuperarse solas. Para las más antiguas, lista los eventos con la API y vuelve a procesarlos. Si quedaron clientes que pagaron sin acceso, la guía de pagos con Stripe en una app hecha con IA explica cómo reparar sus cuentas.

¿Qué no debes hacer mientras lo arreglas?

  • No quites la verificación de firma "por ahora". Sin ella, cualquiera que encuentre la URL de tu webhook puede mandar un checkout.session.completed falso y regalarse un plan pagado.
  • No pegues el secreto en el código del frontend ni en un repositorio público para probar más rápido.
  • No confíes solo en el cuerpo del evento para lo importante. Después de verificar, pedir la suscripción o la sesión a Stripe te da el estado actual. La guía de suscripciones con Stripe usa este patrón.

Respuestas rápidas

En local funciona y en producción falla. ¿Por qué? En local usas el secreto del CLI; producción necesita el secreto del endpoint del Dashboard de ese entorno.

Los eventos del sandbox se verifican y los live no. El endpoint live tiene su propio secreto. Pon el whsec_ live en las variables de producción y despliega.

¿Puede ser que Stripe mande una firma incorrecta? Es muy poco probable. En la práctica, la causa casi siempre está en el secreto, el cuerpo o la cabecera de tu lado.

¿También debo permitir solo las IP de Stripe? Stripe las publica y lo sugiere como capa extra, pero la verificación de firma es el control que importa.

¿Sigues viendo errores de firma?

Si revisaste los cuatro datos y las entregas siguen fallando, normalmente hay algo entre Stripe y tu código, como un proxy o una configuración de la plataforma. Nuestro servicio de pagos con Stripe resuelve justo este tipo de problema; también puedes pasar la revisión gratuita de tu app o contarnos el error. La página de Stripe sobre problemas de firma también vale la pena tenerla guardada.

¿Tu app sigue trabada?

Pasa el chequeo gratis con la URL de tu app, o pide la Auditoría de rescate de $299: un ingeniero senior revisa el código y te envía un informe escrito en 2 días hábiles.