Cómo crear un sitio de suscripción con Stripe y Webflow

Cómo crear un sitio de membresía con suscripción usando Stripe y Webflow

Aprende a restringir contenido en Webflow según el estado de la suscripción en Stripe, gestionando pagos fallidos y cancelaciones a mitad de periodo sin bloquear a los miembros que ya pagaron.

Cómo crear un sitio de membresía con suscripción usando Stripe y Webflow

Ismail Ajagbe
Redactor técnico
Ver perfil del autor
Ismail Ajagbe
Redactor técnico
Ver perfil del autor
Índice de contenidos

Restringe contenido en Webflow según el estado de la suscripción en Stripe, gestionando pagos fallidos y cancelaciones a mitad de periodo sin bloquear a los miembros que ya pagaron.

Lanzar un sitio de suscripción es pan comido. El problema viene después, con la larga lista de estados por los que pasa una membresía.

Sin esta base, funciones sencillas como las cancelaciones a mitad de periodo o los reintentos de pago suelen fallar porque el código depende de indicadores estáticos que se desincronizan de la realidad de Stripe. Al sincronizar los datos de suscripción directamente desde los webhooks, tu aplicación crea una única fuente de verdad que se mantiene precisa incluso cuando cambian los estados de las membresías.

Esta guía construye la capa de suscripción en Webflow Cloud para que el acceso dependa del estado de la suscripción y no de la memoria.

¿Qué necesitas para crear un sitio de suscripción con Stripe en Webflow?

Necesitas una cuenta de Stripe con un precio recurrente, una forma de identificar a los miembros que inician sesión y un lugar donde almacenar el estado de la suscripción que envían los webhooks. Aquí, la capa de cuenta es más importante que la de facturación.

Aquí tienes la lista antes de procesar el primer pago:

  • Una cuenta de Stripe con un producto y un precio recurrente, además de un secreto de firma para los webhooks
  • Un sistema de autenticación funcional, para que cada miembro esté vinculado a un registro de usuario estable
  • Un proyecto de Webflow Cloud que ejecute Next.js 15 o superior, con Node.js 22 o posterior instalado localmente.
  • Un lugar donde persistir el estado de la suscripción, como el binding de SQLite de Webflow Cloud, indexado por el ID de cliente de Stripe

Si aún no has configurado el inicio de sesión, nuestra guía de autenticación y pagos cubre esos fundamentos, y esta guía se basa en ellos. Todo lo que sigue se apoya en una regla: Stripe es el dueño de la verdad sobre la facturación, y tu base de datos solo guarda una copia que nunca debe inventar nada. Aquí te explicamos cómo.

¿Qué estados de suscripción de Stripe deberían permitir el acceso?

Las suscripciones de Stripe pasan por una serie de estados definidos, y el derecho de acceso depende del estado y de la fecha de finalización del periodo, no de un simple indicador. La mayoría de los sitios de membresía se equivocan en dos de estos estados.

Los ocho estados y lo que cada uno debería significar para el acceso:

Estado de la suscripción Qué significa Qué acceso debería tener
incompleto Primera factura aún pendiente; el cliente tiene 23 horas Ninguno, hasta que se active
incomplete_expired Esas 23 horas pasaron; la factura se anuló y este es el estado final Ninguno; el miembro debe iniciar una suscripción nueva
trialing Periodo de prueba en curso, aún no se ha realizado el cobro Completo, para el nivel de prueba
active Pagado y al día Completo
past_due Un pago falló y Stripe está reintentando Tú decides: normalmente se mantiene el acceso mientras se realizan los reintentos
unpaid Se agotaron los reintentos; las facturas se siguen generando, pero no se intenta el cobro Revocar, que es lo que Stripe recomienda para este estado
canceled Finalizado, ya sea de inmediato o al terminar el periodo; estado final Revocar, pero solo una vez que el periodo pagado haya concluido realmente
en pausa Una prueba finalizó sin método de pago y el comportamiento al finalizar está configurado en pausa Ninguno, hasta que se añada una tarjeta y se reanude la suscripción
Estado de la suscripción → Qué significa → Cuál debería ser el acceso
incompleto
Primera factura aún pendiente; el cliente tiene 23 horas
Ninguno, hasta que se active
incomplete_expired
Esas 23 horas pasaron; la factura se anuló y este es el estado final
Ninguno; el miembro debe iniciar una suscripción nueva
trialing
Periodo de prueba en curso, aún no se ha realizado el cobro
Completo, para el nivel de prueba
active
Pagado y al día
Completo
past_due
Un pago falló y Stripe está reintentando
Tú decides: normalmente se mantiene el acceso mientras se realizan los reintentos
unpaid
Se agotaron los reintentos; las facturas se siguen generando, pero no se intenta el cobro
Revocar, que es lo que Stripe recomienda para este estado
canceled
Finalizado, ya sea de inmediato o al terminar el periodo; estado final
Revocar, pero solo una vez que el periodo pagado haya concluido realmente
en pausa
Una prueba finalizó sin método de pago y el comportamiento al finalizar está configurado en pausa
Ninguno, hasta que se añada una tarjeta y se reanude la suscripción

Las dos filas que merecen debate son past_due y canceled. Cuando un pago falla, Stripe realiza reintentos según la configuración de tu panel, y la suscripción pasa entonces a canceled, se mueve a unpaid, o permanece en past_due, dependiendo de esos ajustes. Cortar el acceso al primer fallo bloquea a los miembros cuya tarjeta simplemente caducó, convirtiendo un evento de facturación recuperable en una cancelación.

La fila canceled genera tickets de soporte. Un miembro que cancela a mitad del periodo normalmente ha pagado hasta el final, por lo que revocar el acceso inmediatamente le quita algo que ya compró. Comprobar el final del periodo, y no solo el estado, es lo que hace que se comporte de esa manera.

5 pasos para crear una membresía de suscripción con Stripe y Webflow

La estructura consiste en una sesión de Checkout que identifica a tu miembro, un webhook que registra el estado de la suscripción, una función de derechos a la que todo lo demás llama, el portal del cliente para el autoservicio y una ruta de conciliación para los eventos que nunca llegan.

Cada pieza existe para mantener una única fuente de verdad, así que empieza por la que la crea.

1. Conecta el cliente de Stripe a tu registro de usuario

Antes de que se mueva dinero, decide cómo se vincula un cliente de Stripe con un miembro. Crea el cliente de Stripe cuando se cree la cuenta, no en el momento del pago, y guarda su ID en tu fila de usuario.

Esta unión sustenta todo lo demás. Los webhooks llegan identificados por el ID de cliente de Stripe, así que sin ese mapeo almacenado de antemano, te verás obligado a adivinar a partir de una dirección de correo electrónico, lo cual falla en el momento en que alguien cambia la suya.

Pasa el ID de cliente a la sesión de Checkout cuando el miembro se suscriba en lugar de dejar que Stripe cree un segundo cliente para la misma persona. Los clientes duplicados son lo más tedioso de resolver después, porque la suscripción está en un registro y tu mapeo apunta al otro.

Terminas este paso con cada miembro teniendo exactamente un ID de cliente de Stripe almacenado en su registro.

2. Registra el estado de la suscripción desde un webhook verificado

El ciclo de vida de la suscripción te llega en forma de eventos, y esos eventos son la única señal fiable de que algo ha cambiado. Un miembro que mejora, reduce su plan, cancela o tiene un fallo en el pago, todo llega por aquí.

La verificación tiene un requisito específico de Workers que vale la pena conocer antes de que falle:

// app/api/stripe-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
  maxNetworkRetries: 2,
})

export async function POST(request: NextRequest) {
  const signature = request.headers.get('stripe-signature')
  if (!signature) {
    return NextResponse.json({ error: 'Unsigned request' }, { status: 400 })
  }

  // Raw text. Parsing first changes the bytes the signature covers.
  const body = await request.text()

  let event: Stripe.Event
  try {
    // Async form: the Workers build uses Web Crypto, which cannot
    // verify synchronously.
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET as string
    )
  } catch {
    return NextResponse.json({ error: 'Bad signature' }, { status: 400 })
  }

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated':
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription

      // Store the state, not a boolean. "Is this member allowed in"
      // is a question you answer at read time from these fields.
      await saveSubscriptionState({
        customerId: sub.customer as string,
        status: sub.status,
        // Removed from the subscription resource in 2025-03-31.basil
        // and moved onto the item. cancel_at_period_end stayed put.
        currentPeriodEnd: sub.items.data[0]?.current_period_end ?? null,
        cancelAtPeriodEnd: sub.cancel_at_period_end,
        priceId: sub.items.data[0]?.price.id ?? null,
      })
      break
    }
    default:
      break
  }

  return NextResponse.json({ received: true })
}

Ten en cuenta también lo que estás almacenando. No un booleano, sino el estado, el final del periodo, el indicador de cancelación y el precio. Esos cuatro campos te permiten responder a cualquier pregunta sobre derechos más adelante, incluso a las que aún no se te han ocurrido, mientras que un booleano descarta esa información en el momento en que llega.

El formulario de verificación asíncrona es obligatorio, no una cuestión de estilo. En el entorno de ejecución de Workers, la biblioteca de Stripe selecciona un proveedor de Web Crypto que solo funciona de forma asíncrona. Por lo tanto, la llamada síncrona constructEvent lanza un error en lugar de verificar, y la propia plantilla de Cloudflare Worker de Stripe utiliza la llamada asíncrona por la misma razón.

Leer el cuerpo como texto sin formato es importante por la misma razón que en cualquier otro lugar: volver a serializar cambia los bytes que cubre la firma.

3. Deriva el acceso en lugar de almacenarlo

Con el estado registrado, el derecho de acceso se convierte en una función pura. Un solo lugar responde si un miembro tiene permiso para entrar, y cada página y ruta lo consulta en lugar de volver a implementar la regla.

La lógica es breve, y los comentarios son la parte interesante:

// lib/entitlement.ts
type SubscriptionState = {
  status: string
  currentPeriodEnd: number | null // unix seconds, from items.data[0]
  cancelAtPeriodEnd: boolean
}

export function hasAccess(state: SubscriptionState | null): boolean {
  if (!state || state.currentPeriodEnd === null) return false

  const periodStillRunning = state.currentPeriodEnd * 1000 > Date.now()

  switch (state.status) {
    case 'active':
    case 'trialing':
      // A cancellation scheduled for period end is still active today.
      return true

    case 'past_due':
      // Stripe is still retrying. Cutting access here punishes people
      // whose card expired, so keep them in while the retries run.
      return periodStillRunning

    case 'canceled':
      // Cancelled at period end means they paid for this period.
      return periodStillRunning

    default:
      // incomplete, incomplete_expired, unpaid, paused, and later additions.
      return false
  }
}

Stripe ofrece una primitiva diseñada específicamente para esto. Cuando una suscripción se activa, crea un derecho de acceso activo para cada función del producto suscrito, y puedes leerlos en lugar de derivar el acceso tú mismo.

Vale la pena conocerlo, pero también omitirlo aquí: los casos complicados, como las cancelaciones a mitad de periodo, son decisiones de política que querrás ver en tu propio código en lugar de que sean inferidas.

Mantener esto en una sola función es lo que hace que la política sea fácil de cambiar. Si más tarde decides darles a past_due los miembros un periodo de gracia más corto, o restringir el acceso de los usuarios en prueba a un área premium, solo tendrás que editar un archivo en lugar de auditar cada ruta que haya verificado un indicador.

El caso por defecto también importa. Tratar los estados desconocidos como "sin acceso" significa que cualquier estado que Stripe introduzca en el futuro fallará de forma segura en lugar de conceder acceso silenciosamente. Después de este paso, podrás explicar, para cualquier miembro, exactamente por qué puede o no puede ver una página.

4. Delega los cambios de facturación al portal del cliente

Los miembros querrán actualizar su tarjeta, cambiar de plan, descargar una factura y cancelar. Crear esos flujos lleva semanas, y Stripe ya los aloja.

Crea una sesión de portal y redirige:

// app/api/billing-portal/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
import { getSessionCustomerId } from '@/lib/session'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string)

export async function POST() {
  // Derive the customer from the signed-in session, never from
  // the request body: an ID taken from the client lets anyone
  // open anyone else's billing portal.
  const customerId = await getSessionCustomerId()

  if (!customerId) {
    return NextResponse.json({ error: 'Not signed in' }, { status: 401 })
  }

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_SITE_URL}/account`,
  })

  // The URL is short-lived, so redirect now rather than storing it.
  return NextResponse.json({ url: session.url })
}

Aquí hay dos puntos clave. El ID del cliente proviene de la sesión iniciada y no del cuerpo de la solicitud, porque aceptar esto desde el cliente permitiría que cualquiera abra el portal de facturación de otro usuario simplemente editando un valor. Además, la URL de la sesión tiene una vida útil corta por diseño, así que crea una por cada solicitud en lugar de almacenarla en caché.

El Dashboard configura lo que el portal puede hacer, no el código, así que decide allí si los miembros pueden cambiar de plan por sí mismos o solo cancelar. Termina este paso con una página de cuenta cuyo botón de facturación enlace a algo que no tuviste que construir.

5. Concilia, porque no se garantiza que los webhooks lleguen

Todo lo anterior asume que los eventos llegan. Casi siempre lo hacen, pero un despliegue durante la entrega o un endpoint que devuelve errores durante una hora dejará tu copia de la realidad desfasada respecto a la de Stripe.

Añade una ruta de conciliación: cuando la página de cuenta de un miembro se cargue y su fecha de fin de periodo almacenada esté en el pasado, vuelve a leer la suscripción desde Stripe y actualiza el registro. Esto cuesta una llamada a la API en una página que nadie carga a menudo, y corrige automáticamente el caso exacto en el que un evento perdido bloquearía a un miembro de pago.

Esta misma lectura ayuda al soporte técnico. Cuando alguien dice que su acceso es incorrecto, comparar tu estado almacenado con una lectura actualizada te indica de inmediato si el problema es un evento perdido o una decisión de política, que son errores muy distintos.

Terminas este paso con un sistema que se corrige a sí mismo, no uno que necesita que un humano se dé cuenta.

¿Qué causa que el acceso a la suscripción de Stripe falle en Webflow Cloud?

Los errores de suscripción rara vez son evidentes. El miembro ve algo incorrecto, los registros muestran una solicitud exitosa y nada sale a la luz hasta que alguien se queja o una factura parece errónea.

Estos cuatro explican la mayoría de ellos, y los dos primeros suelen llegar a las bandejas de entrada de soporte.

Un miembro cancelado pierde el acceso de inmediato

Causa: La lógica de derechos se basa incorrectamente solo en el estado de la suscripción, lo que provoca una revocación inmediata cuando el estado se marca como "cancelado". En realidad, cuando un miembro cancela a mitad de período, normalmente ya ha pagado el resto de su ciclo de facturación.

Stripe registra esto como una cancelación efectiva al final del período, no como una terminación inmediata, por lo que su sistema está bloqueando prematuramente a clientes que ya han pagado.

Solución: Verifique el final del período actual junto con el estado y mantenga el acceso hasta que pase esa marca de tiempo. Lo mismo se aplica a ser verdadero en una suscripción que de otro modo estaría activa: significa que se ha tomado una decisión sobre el futuro, no sobre el presente.cancel_at_period_end

Pruébelo cancelando una suscripción de prueba en el Panel de control y confirmando que el miembro mantiene el acceso hasta que el período realmente finalice.

Un pago fallido bloquea a un miembro que paga

Causa: Tratar como equivalente a cancelado. Cuando una tarjeta falla, Stripe vuelve a intentar el cobro según el programa establecido en su Panel de control, y la suscripción sigue siendo recuperable durante ese período en lugar de haber terminado.past_due

Solución: Mantenga el acceso durante el período de reintento y aproveche ese tiempo para solicitar al miembro que actualice sus datos, que es para lo que existe el portal del cliente. Revoque el acceso en o , según a cuál de ellos envíe su Panel de control los reintentos agotados, en lugar de hacerlo en el primer fallo.unpaidcanceled

Stripe es directo respecto a lo primero: revoque el acceso cuando una suscripción no esté pagada, ya que es cuando ya se han intentado y reintentado los pagos mientras estaba vencida.

Decida esto de forma deliberada y no por defecto, ya que la diferencia entre ambas políticas se mide en bajas involuntarias. El programa de reintentos es una configuración del Panel de control, no algo que su código controle, así que compruebe cómo está configurado el suyo antes de elegir un período de gracia, o ambos entrarán en conflicto.

Un miembro mantiene el acceso después de que su período haya terminado

Causa: Estado almacenado que nunca se actualizó. La suscripción terminó en Stripe, pero el evento que debería haberle avisado falló en la entrega, llegó durante un despliegue o alcanzó un punto final que devolvía errores en ese momento. Su base de datos aún conserva el último estado del que fue informada, y el derecho de acceso se deriva de un final de período que ya ha pasado.

Solución: Añada la lectura de conciliación del paso 5, de modo que un final de período almacenado en el pasado provoque una nueva consulta a Stripe en lugar de confiar en él. Compruebe el historial de entrega del punto final del webhook en el Panel de control para el cliente afectado, ya que eso distingue un evento perdido de una decisión de política que funciona según lo diseñado.

Si las entregas fallan de forma generalizada y no solo para un miembro, arregle el punto final antes de reparar registros individuales, o se pasará toda la semana reparándolos.

La compilación falla después de añadir las rutas de facturación

Causa: Una directiva en uno de los archivos nuevos. Webflow Cloud despliega Next.js a través del export const runtime = 'edge'adaptador OpenNext Cloudflare, que no es compatible con el entorno de ejecución edge de Next.js.

Solución: Elimine la línea y vuelva a desplegar, ya que los controladores de ruta (Route Handlers) ya se ejecutan en el entorno de ejecución de Workers sin ella. Busque en todo el proyecto, no solo en el archivo que editó por última vez, porque una sola directiva en cualquier lugar hará que la compilación falle.

Compruebe esto primero cuando una compilación se rompa justo después de añadir nuevas rutas, ya que la propia página de "trae tu propia aplicación" de Webflow todavía indica a los lectores de Next.js que la añadan, y los ejemplos de pago de otros proveedores a menudo también la incluyen.

Lo que puede construir a continuación con Stripe y Webflow

Una vez que los derechos de acceso se derivan en lugar de almacenarse, las adiciones se vuelven más sencillas: acceso por niveles según el ID de precio, planes basados en puestos donde una suscripción cubre varios inicios de sesión, o un período de gracia que degrada las funciones en lugar de cortarlas por completo.

Si tus precios son variables en lugar de fijos, nuestra guía de facturación basada en el uso cubre la parte de medición y qué productos de uso de Stripe puedes utilizar como base. Para las rutas de conexión sin código, consulta la integración de Webflow y Stripe.

Para una personalización más profunda más allá de lo que cubren esas opciones, la documentación para desarrolladores de Webflow detalla los controladores de ruta, los enlaces de almacenamiento y el resto del entorno de ejecución de Webflow Cloud.

Preguntas frecuentes

¿Debería guardar un valor booleano para saber si alguien es miembro?

No. Guarda el estado de la suscripción, la fecha de finalización del periodo actual, el indicador de cancelación y el precio, y luego deduce el acceso a partir de ellos. Un valor booleano descarta la información que necesitas para los casos complicados y se desincroniza de Stripe en cuanto falla un pago.

¿Qué debería pasar cuando falla un pago?

Por lo general, mantén el acceso mientras Stripe realiza reintentos. Una tarjeta rechazada es un evento recuperable, y la suscripción pasa a canceled, pasa a unpaid, o permanece en past_due, según la configuración de tu Dashboard. Revocar el acceso al primer fallo convierte una tarjeta caducada en una cancelación.

¿Un miembro que cancela pierde el acceso de inmediato?

No, si canceló a mitad del periodo. Normalmente, la cancelación se programa para el final del periodo pagado, así que verifica la fecha de finalización junto con el estado y mantén el acceso hasta que pase esa fecha.

¿Necesito crear pantallas de gestión de facturación?

Casi nunca. El portal de clientes de Stripe gestiona las actualizaciones de métodos de pago, facturas, cambios de plan y cancelaciones; lo que permite se configura desde el Dashboard. Crea una sesión de portal por cada solicitud, ya que la URL devuelta tiene una vida útil corta.

¿Qué pasa si nunca llega un webhook?

Concilia al leer. Cuando haya pasado la fecha de finalización del periodo guardada para un miembro, vuelve a consultar la suscripción en Stripe y actualiza tu registro. Esa simple llamada soluciona el caso de eventos perdidos sin que nadie se dé cuenta.


Última actualización
February 6, 2026
Categoría

Artículos relacionados


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo

Empieza gratis

Prueba Webflow todo el tiempo que quieras con nuestro plan Starter gratuito. Compra un plan de sitio de pago para publicar, alojar y desbloquear funciones adicionales.

Empieza, es gratis
Ver demostración

Prueba Webflow todo el tiempo que quieras con nuestro plan Starter gratuito. Compra un plan de sitio de pago para publicar, alojar y desbloquear funciones adicionales.