Cómo integrar los cursos online de Kajabi con Webflow Cloud usando la API pública

Cómo integrar cursos online de Kajabi con Webflow Cloud usando la API pública

Conecta los cursos online de Kajabi a una aplicación de Webflow Cloud con la API pública, añadiendo visualización del catálogo desde el servidor, comprobaciones de acceso y concesión de ofertas.

Cómo integrar cursos online de Kajabi con Webflow Cloud usando la API pública

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

Vender un curso de Kajabi en tu propio sitio suele terminar en un incrustado frágil; conecta la API pública de Kajabi a una aplicación de Webflow Cloud y tendrás el control total del catálogo, las comprobaciones de acceso y las inscripciones.

Kajabi ofrece una API pública real, y Webflow Cloud ejecuta tu propio código en Cloudflare Workers en el borde, dándote un hogar en el servidor para tus credenciales y llamadas a la API. Júntalo todo y la integración dejará de ser una serie de incrustados frágiles para convertirse en una aplicación que tú controlas.

En esta guía, construiremos esa integración paso a paso, desde la autenticación en Kajabi hasta un webhook que mantiene el acceso al curso sincronizado sin importar dónde se realice la compra.

¿Qué necesitas para integrar los cursos de Kajabi con Webflow Cloud?

Necesitas una aplicación Next.js de Webflow Cloud, una cuenta de Kajabi con el plan que incluye la API pública y una clave de API de usuario de Kajabi. La API es el requisito indispensable. Está incluida en el plan Pro de Kajabi y, si no, se vende como un complemento de 25 dólares al mes, así que confirma que tu cuenta la tiene antes de empezar a conectar llamadas.

Aquí tienes la lista completa de lo que necesitas:

  • Un proyecto de Webflow Cloud ejecutando una aplicación Next.js, ya sea desplegada o en desarrollo local
  • Una cuenta de Kajabi con acceso a la API pública (el nuevo plan Pro o el complemento de API pública)
  • Una clave de API de usuario de Kajabi (client_id y client_secret) creada en Ajustes > API pública (app.kajabi.com)
  • Al menos una oferta de Kajabi que conceda acceso al curso que quieres vender o restringir
  • Node.js 22.13.0 o superior de forma local, el requisito mínimo para Webflow CLI 2.0

La oferta es lo que la gente suele olvidar. En Kajabi, un curso es el contenido y una oferta es lo que realmente concede el acceso a él, por lo que cada acción de "inscribir a esta persona" en la API es, en realidad, "conceder esta oferta".

Una vez que todo esto está listo, la integración es solo un puñado de manejadores de rutas, y normalmente consigo que el primero devuelva datos reales de Kajabi en menos de una hora.

6 pasos para integrar los cursos online de Kajabi con una aplicación de Webflow Cloud

La integración cubre todo el ciclo de vida de un curso en tu sitio:

  • Crear una clave de API
  • Autenticarse en Kajabi
  • Listar cursos
  • Comprobar si el miembro actual tiene acceso
  • Conceder acceso tras una compra
  • Mantener todo sincronizado cuando Kajabi lanza un evento

Cada paso se ejecuta en un Route Handler en el entorno de ejecución de Node.js que Webflow Cloud proporciona a través del adaptador OpenNext Cloudflare, por lo que el client_secret y el token de acceso permanecen en el lado del servidor y nunca llegan al navegador.

El orden importa porque cada paso se basa en el anterior. Todos los controladores posteriores importan la utilidad de token creada en el segundo paso, así que ahí es donde empieza el trabajo de verdad.

1. Crear una clave de API de usuario de Kajabi

Inicia sesión en app.kajabi.com y ve a Settings > Public API. En esta página es donde creas las credenciales que utiliza la API, y donde puedes rotarlas o eliminarlas más adelante. Haz clic en Create User API Key, asígnale un nombre que identifique el proyecto (por ejemplo, webflow-cloud), selecciona el usuario y los permisos que debe tener la clave, y haz clic en Create.

Kajabi te mostrará un client_id y un client_secret. Cópialos ahora, porque el secreto solo es totalmente visible en el momento de la creación. Si alguna vez se filtra, la misma página te permite rotar la clave, lo que invalida todos los tokens de acceso emitidos con el secreto antiguo sin necesidad de reconstruir la integración.

Añade ambos valores a .env.local y a tu proyecto de Webflow Cloud en Settings > Environment Variables:

KAJABI_CLIENT_ID=your_client_id
KAJABI_CLIENT_SECRET=your_client_secret

Ninguno de los valores lleva el prefijo NEXT_PUBLIC_. El navegador nunca accede a ellos; solo los Route Handlers los leen. Tratar el client_secret como un valor exclusivo del servidor es el hábito más importante de toda esta integración, y es la razón por la que la ruta de la API es más segura que cualquier elemento incrustado en el cliente.

2. Crear una utilidad de token OAuth de Kajabi para Webflow Cloud

Cada llamada autenticada a Kajabi requiere un token de acceso Bearer, que obtienes intercambiando tus credenciales en el endpoint /v1/oauth/token usando la concesión client_credentials.

En lugar de realizar ese intercambio en cada controlador, lo mantengo en una única utilidad y guardo el token en caché en la memoria durante la vida útil del aislamiento, ya que los tokens son válidos durante un número fijo de segundos y volver a solicitar uno en cada llamada es un desperdicio.

Crea :lib/kajabi.ts

// lib/kajabi.ts

const KAJABI_API = 'https://api.kajabi.com/v1'

type TokenResponse = {
  access_token: string
  refresh_token: string
  token_type: 'Bearer'
  expires_in: number
}

// Cache the token in module scope. It survives for the life of the
// Worker isolate, so warm requests skip the token exchange entirely.
let cached: { token: string; expiresAt: number } | null = null

async function getAccessToken(): Promise<string> {
  // Reuse the cached token until 60s before it expires.
  if (cached && Date.now() < cached.expiresAt - 60_000) {
    return cached.token
  }

  const body = new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: process.env.KAJABI_CLIENT_ID!,
    client_secret: process.env.KAJABI_CLIENT_SECRET!,
  })

  const res = await fetch(`${KAJABI_API}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body,
  })

  if (!res.ok) {
    throw new Error(`Kajabi token request failed: ${res.status}`)
  }

  const data = (await res.json()) as TokenResponse
  cached = {
    token: data.access_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  }
  return data.access_token
}

// A thin wrapper so handlers never deal with tokens or base URLs directly.
export async function kajabiFetch(
  path: string,
  init: RequestInit = {}
): Promise<Response> {
  const token = await getAccessToken()
  return fetch(`${KAJABI_API}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/vnd.api+json',
      ...init.headers,
    },
  })
}

Hay dos cosas en este código que hacen el trabajo real:

(1) La solicitud de token es application/x-www-form-urlencoded, no JSON, porque eso es lo que espera el endpoint de OAuth; enviar JSON es la razón más común por la que falla una primera llamada de token.

(2) La cabecera Accept: application/vnd.api+json también es intencionada, porque la API de Kajabi sigue la especificación JSON:API y devuelve recursos bajo una clave data con attributes y relationships separados. Configurarla una vez en el envoltorio significa que cada controlador posterior recibe respuestas con el formato correcto sin tener que repetir la cabecera.

Esta utilidad es la base. A partir de aquí, ningún controlador se comunica directamente con fetch; todos llaman a kajabiFetch, lo que significa que el manejo de tokens, la URL base y el tipo de medio residen en un solo lugar.

3. Mostrar el catálogo de cursos de Kajabi desde un Route Handler

Lo primero útil que puede hacer tu sitio de Webflow es mostrar la lista real de cursos directamente desde Kajabi, para que el catálogo nunca esté desactualizado respecto a lo que realmente vendes. El endpoint es GET /v1/courses, y admite paginación y filtrado, además de conjuntos de campos dispersos para recortar la carga útil, todo lo cual rediriges a través de un Route Handler para que el navegador solo vea los campos que decides exponer.

Crea :app/api/courses/route.ts

// app/api/courses/route.ts

import { kajabiFetch } from '@/lib/kajabi'
import { NextResponse } from 'next/server'

type KajabiCourse = {
  id: string
  type: 'courses'
  attributes: {
    title: string
    description: string
    status: string
    thumbnail_url?: string
  }
}

export async function GET() {
  // fields[courses] keeps the payload small; only ask for what the UI renders.
  const res = await kajabiFetch(
    '/courses?fields[courses]=title,description,thumbnail_url,status&page[size]=50'
  )

  if (!res.ok) {
    return NextResponse.json(
      { error: 'Failed to load courses' },
      { status: 502 }
    )
  }

  const { data } = (await res.json()) as { data: KajabiCourse[] }

  // Flatten the JSON:API shape into something the frontend can map over directly.
  const courses = data
    .filter((c) => c.attributes.status === 'active')
    .map((c) => ({
      id: c.id,
      title: c.attributes.title,
      description: c.attributes.description,
      thumbnail: c.attributes.thumbnail_url ?? null,
    }))

  return NextResponse.json({ courses })
}

El parámetro fields[courses] es la parte que vale la pena conservar. Kajabi devuelve un recurso completo por defecto, y limitarlo a los cuatro atributos que necesita la interfaz mantiene la respuesta pequeña y garantiza que nunca envíes accidentalmente campos internos al cliente.

Filtrar por status === 'active' en el lado del servidor sigue la misma lógica: los borradores permanecen privados sin necesidad de lógica en el frontend.

Desde cualquier componente de cliente en tu aplicación de Webflow Cloud, el catálogo es ahora una simple petición a tu propio endpoint:

// In a Client Component
const res = await fetch('/api/courses')
const { courses } = await res.json()
// Render courses; the Kajabi token never left the server.

Como la solicitud pasa por tu Route Handler en lugar de ir directamente a Kajabi, el token de acceso permanece en el servidor y tienes un único lugar donde añadir caché o filtros más adelante. Con el catálogo renderizado, la siguiente pregunta es la que los elementos incrustados nunca pueden responder: ¿la persona que lo está viendo tiene acceso realmente?

4. Verifica el acceso de un miembro al curso desde el lado del servidor

Las comprobaciones de acceso deben ejecutarse en el servidor, punto final. Ocultar un botón de "Continuar aprendiendo" con JavaScript no es control de acceso, porque cualquiera puede abrir las herramientas de desarrollo y ver lo que se ocultó. El enfoque correcto es preguntar a Kajabi, desde un manejador de rutas, si a una persona determinada se le ha concedido la oferta que abre el curso, y devolver solo un sí o un no al navegador.

El correo electrónico debe provenir de una sesión verificada por tu parte, nunca de un parámetro de consulta que controle quien realiza la llamada. Un manejador que responda a cualquier dirección es tanto un oráculo de listas de clientes como una vulnerabilidad, ya que un atacante puede simplemente pasar la dirección de un comprador conocido. Lee la dirección desde tu propia cookie de sesión o token, y trata el parámetro de consulta a continuación solo como ilustrativo.

Kajabi modela esto en el contacto.

Buscas el contacto por correo electrónico, limitado a tu sitio, y preguntas si posee la oferta detrás del curso usando el parámetro .filter[has_offer_id]

Crea :app/api/access/route.ts

// app/api/access/route.ts

import { kajabiFetch } from '@/lib/kajabi'
import { NextResponse, type NextRequest } from 'next/server'

type Contact = { id: string; type: 'contacts' }

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const email = searchParams.get('email')
  const offerId = searchParams.get('offerId')

  if (!email || !offerId) {
    return NextResponse.json({ error: 'Missing email or offerId' }, { status: 400 })
  }

  // Ask Kajabi for contacts on this site who hold the offer AND match the email.
  const query = new URLSearchParams({
    'filter[site_id]': process.env.KAJABI_SITE_ID!,
    'filter[has_offer_id]': offerId,
    'filter[email_contains]': email,
  })

  const res = await kajabiFetch(`/contacts?${query.toString()}`)

  if (!res.ok) {
    return NextResponse.json({ error: 'Lookup failed' }, { status: 502 })
  }

  const { data } = (await res.json()) as { data: Contact[] }

  // filter[email_contains] is a partial match, so a non-empty array is not an
  // answer on its own. Confirm the exact address before granting access.
  const target = email.trim().toLowerCase()
  const hasAccess = data.some(
    (c) => c.attributes.email.toLowerCase() === target
  )

  return NextResponse.json({ hasAccess })
}

Añade a tus variables de entorno; puedes leerlo desde la relación de cualquier recurso o el punto de conexión . La combinación de tres filtros es lo que hace que esto sea confiable:KAJABI_SITE_IDsite/v1/sites

  • filter[site_id]limita la búsqueda a tu sitio
  • filter[has_offer_id]lo restringe a las personas que realmente poseen la oferta detrás del curso
  • filter[email_contains]lo reduce a la persona que realiza la consulta

Una dirección exacta coincidente en la matriz es un resultado de acceso positivo, y el navegador solo ve .data{ hasAccess: true }

La parte crítica es lo que haces con la respuesta. El valor booleano decide si tu servidor renderiza o devuelve el recurso protegido; hace más que simplemente alternar una clase CSS. Si el contenido protegido es una URL de video de lección o una descarga, sírvelo desde este mismo manejador solo cuando sea verdadero, de la misma manera que protegerías cualquier recurso mediante una validación de sesión del lado del servidor.hasAccess

De esa manera, el contenido nunca permanece en la página esperando ser revelado. Las cuentas de Kajabi más recientes también pueden usar para verificar la propiedad del producto directamente, lo cual es útil cuando un solo curso se vende a través de múltiples ofertas.filter[has_active_product_id]

Ten en cuenta que la propiedad del producto y el acceso a la oferta no son idénticos, así que usa el filtro de producto solo cuando quieras restringir específicamente el acceso al producto subyacente en lugar de a la oferta.

5. Conceder acceso al curso después de una compra

Cuando alguien compra un curso en tu sitio de Webflow, debes concederle la oferta de Kajabi correspondiente para que el curso aparezca en su biblioteca de Kajabi. Eso es una única a la relación de ofertas del contacto, y Kajabi se encarga del resto: crea un registro de cliente si no existe, envía el correo electrónico de bienvenida y activa cualquier automatización vinculada a la oferta.POST

El flujo consiste en encontrar o crear el contacto y luego conceder la oferta.

Aquí tienes :app/api/enroll/route.ts

// app/api/enroll/route.ts

import { kajabiFetch } from '@/lib/kajabi'
import { NextResponse, type NextRequest } from 'next/server'

type Contact = { id: string; type: 'contacts' }

async function findOrCreateContact(email: string, name: string): Promise<string> {
  const siteId = process.env.KAJABI_SITE_ID!

  // 1. Look for an existing contact by email on this site.
  const lookup = await kajabiFetch(
    `/contacts?filter[site_id]=${siteId}&filter[email_contains]=${encodeURIComponent(email)}`
  )
  const { data } = (await lookup.json()) as { data: Contact[] }
  if (data.length > 0) return data[0].id

  // 2. None found: create one. JSON:API requires the vnd.api+json content type.
  const create = await kajabiFetch('/contacts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/vnd.api+json' },
    body: JSON.stringify({
      data: {
        type: 'contacts',
        attributes: { name, email },
        relationships: {
          site: { data: { type: 'sites', id: siteId } },
        },
      },
    }),
  })
  const created = (await create.json()) as { data: Contact }
  return created.data.id
}

export async function POST(request: NextRequest) {
  // In production, only call this AFTER you have verified the payment.
  const { email, name, offerId } = (await request.json()) as {
    email: string
    name: string
    offerId: string
  }

  const contactId = await findOrCreateContact(email, name)

  // Grant the offer. The body is a JSON:API resource identifier array.
  const grant = await kajabiFetch(`/contacts/${contactId}/relationships/offers`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/vnd.api+json' },
    body: JSON.stringify({
      data: [{ type: 'offers', id: offerId }],
      meta: { send_customer_welcome_email: true },
    }),
  })

  if (!grant.ok) {
    return NextResponse.json({ error: 'Grant failed' }, { status: 502 })
  }

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

La bandera es la palanca que la mayoría quiere accionar. Déjala en , y Kajabi enviará su correo electrónico de bienvenida estándar; establécela en cuando tu aplicación de Webflow ya envíe su propia secuencia de incorporación y no quieras que el comprador reciba dos.meta.send_customer_welcome_emailtruefalse

La llamada de concesión también es idempotente en el lado de Kajabi, por lo que verifica si el contacto ya posee la oferta antes de crear un duplicado.

Una regla lo anula todo en este manejador: nunca lo expongas como un punto de conexión abierto. Conceder una oferta es regalar contenido de pago, por lo que esto solo debe ejecutarse después de que tu servidor haya confirmado un pago real, por ejemplo, dentro de un flujo de éxito de Stripe Checkout o detrás de la misma autenticación que protege el resto de tu aplicación. Trata como código privilegiado, no como una ruta pública. Deriva el ID de la oferta desde el lado del servidor a partir del registro de pago verificado en lugar de aceptarlo desde el cuerpo de la solicitud, o cualquier persona que llegue a la ruta podrá elegir la oferta que desee./api/enroll

6. Sincroniza eventos de Kajabi con un Route Handler de webhook

Las compras no siempre se originan en tu sitio de Webflow.

Alguien podría comprar directamente a través de una página de ventas de Kajabi o renovar una suscripción, y también ocurren reembolsos, por lo que tu aplicación debe estar al tanto. Los webhooks salientes de Kajabi cubren esto. Creas uno con POST /v1/hooks, pasando un target_url, una site relación y un event como purchase o payment_succeeded, y Kajabi envía un cuerpo JSON a esa URL cuando se dispara el evento. Las URLs de activación y desactivación de una oferta funcionan en la dirección opuesta: son URLs entrantes a las que haces POST para conceder o revocar el acceso, por lo que no pueden notificar a tu aplicación.

Un Route Handler es el lugar ideal para esa URL.

Crea :app/api/webhooks/kajabi/route.ts

// app/api/webhooks/kajabi/route.ts

import { kajabiFetch } from '@/lib/kajabi'
import { NextResponse, type NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  // 1. Gate the endpoint with a shared secret in the URL, since Kajabi's
  //    offer webhooks carry no documented signature to verify. Set this when you save the hook URL.
  const { searchParams } = new URL(request.url)
  if (searchParams.get('token') !== process.env.KAJABI_WEBHOOK_TOKEN) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  // Hooks created through POST /v1/hooks deliver a JSON body, not form data.
  const body = (await request.json()) as {
    event?: string
    payload?: { member?: { email?: string }; email?: string }
  }
  const email = body.payload?.member?.email ?? body.payload?.email ?? ''
  if (!email) {
    return NextResponse.json({ error: 'No email in payload' }, { status: 400 })
  }

  // 2. Do not trust the payload alone. Re-query Kajabi to confirm the
  //    current state before changing anything in your own store.
  const siteId = process.env.KAJABI_SITE_ID!
  const offerId = process.env.KAJABI_COURSE_OFFER_ID!
  const verify = await kajabiFetch(
    `/contacts?filter[site_id]=${siteId}&filter[has_offer_id]=${offerId}&filter[email_contains]=${encodeURIComponent(email)}`
  )
  const { data } = (await verify.json()) as { data: unknown[] }
  const hasAccess = data.length > 0

  // 3. Persist the confirmed state (KV, a database, the Webflow CMS, etc.).
  // await saveEnrollment(email, hasAccess)

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

La verificación del secreto compartido en la primera línea no es opcional, y es la parte que veo que se omite con más frecuencia. Los webhooks de activación y desactivación de ofertas de Kajabi no te proporcionan una firma documentada para verificar, por lo que un endpoint sin protección es algo a lo que cualquiera que adivine la URL puede hacer POST.

Poner una token cadena aleatoria larga en la cadena de consulta y rechazar cualquier cosa que no la tenga te ofrece una barrera sencilla pero efectiva. He dejado de confiar por completo en los cuerpos de los webhooks por sí solos; la nueva consulta en el segundo paso significa que incluso una carga útil falsificada no puede conceder acceso, porque la decisión proviene del propio estado de Kajabi, no de la solicitud.

Dónde persistas el resultado depende de tu aplicación. Un almacén KV o una base de datos serverless como Neon es el hogar natural para un registro de inscripción, y vale la pena envolver este manejador en un sistema de seguimiento de errores, porque un webhook que falla silenciosamente es la forma en que el acceso pierde la sincronización.

Con el webhook cerrando el círculo, la integración ahora refleja la realidad de Kajabi, independientemente de si la compra comenzó en tu sitio o en el de ellos.

¿Qué es lo que rompe una integración entre Kajabi y Webflow Cloud?

La mayoría de los fallos aquí se deben a unas pocas causas predecibles:

  • El tipo de contenido incorrecto en la solicitud de token
  • Credenciales faltantes o que no coinciden entre los entornos local y desplegado
  • El tipo de medio JSON:API se está perdiendo, o un endpoint de webhook abierto

Cada uno tiende a fallar silenciosamente, devolviendo un estado de error sin una causa obvia.

Los síntomas a continuación corresponden a esas causas, comenzando por la que detiene todo antes de que empiece.

Las solicitudes de token devuelven 401 o invalid_client

La llamada /v1/oauth/token está fallando en la autenticación. Dos causas explican casi todos estos casos. Primero, el cuerpo de la solicitud debe ser application/x-www-form-urlencoded, no JSON; si envías un cuerpo JSON, Kajabi no puede leer las credenciales y rechaza la solicitud.

Confirma que estás construyendo el cuerpo con URLSearchParams y configurando el encabezado Content-Type correspondiente.

Segundo, verifica las credenciales en sí. Una clave rotada invalida todos los tokens del secreto antiguo, así que si la integración funcionaba ayer y hoy no, es posible que alguien haya rotado la clave en Ajustes > API Pública.

Vuelve a copiar el client_secret actual en tus variables de entorno de Webflow Cloud y vuelve a desplegar, ya que un valor que solo existe en .env.local no está presente en el Worker desplegado.

Las llamadas a cursos o contactos devuelven un error 403 de acceso denegado

El token es válido, pero la clave de API de usuario no tiene los permisos necesarios para el endpoint. Kajabi limita cada clave al usuario y a los permisos que seleccionaste al crearla, por lo que una clave diseñada para leer contactos no puede otorgar ofertas.

Regresa a Ajustes > API pública, abre la clave y confirma que tiene permisos para todos los recursos que utilizan tus controladores: al menos cursos y contactos, además de ofertas para la concesión del paso cinco. Si más adelante restringes los permisos por seguridad, ten en cuenta que el controlador correspondiente empezará a devolver errores 403 hasta que actualices la clave.

También recibirás un 403 si la clave pertenece a un usuario que no puede ver el recurso en el sitio en cuestión. Por lo tanto, si un sitio funciona y otro no, comprueba que el usuario de la clave tenga acceso a ambos antes de asumir que los ámbitos de permiso son incorrectos.

Las lecturas devuelven un formato incorrecto o atributos vacíos

Falta la cabecera JSON:APIAccept. Sin Accept: application/vnd.api+json, la API puede devolver una representación distinta y tu código que lee data[].attributes.title no encuentra nada donde espera un valor.

Por eso el contenedorkajabiFetch establece la cabecera en cada solicitud. Si omitiste el contenedor para una llamada puntual, vuelve a añadir la cabecera. Recuerda que los recursos se devuelven bajo data con attributes y relationships como objetos separados, por lo que course.title es siempre course.attributes.title, nunca un campo de nivel superior.

Lo mismo ocurre con los datos relacionados. Cuando solicitas include=lessons?include=modules,lessons, esos registros relacionados llegan en una matriz included de nivel superior en lugar de estar anidados dentro del curso, así que debes vincularlos mediante los ID que aparecen en relationships del curso.

Leer desde el lugar equivocado es la razón más común por la que un curso se carga, pero sus lecciones aparecen vacías.

La concesión de una oferta devuelve un error 400 o crea clientes duplicados

El cuerpo de la solicitud de concesión está mal formado o se omitió la búsqueda del contacto. El cuerpo debe ser una matriz de identificadores de recursos JSON:API, data{ "data": [{ "type": "offers", "id": "OFFER_ID" }] }, enviada con el tipo de contenido application/vnd.api+json application/vnd.api+json. Un objeto { offerId } plano devuelve un código de estado 400.

Los clientes duplicados aparecen cuando se crea un contacto nuevo en lugar de reutilizar el existente. Ejecuta siempre la búsqueda find-or-createfilter[email_contains] primero y crea un contacto solo cuando no se encuentre ninguno, tal como hace el asistente findOrCreateContact de Kajabi.

Kajabi hace coincidir a los clientes por correo electrónico durante una concesión, así que una búsqueda limpia mantiene un solo registro por comprador. Ten cuidado también con la búsqueda en sí: emailfilter[email_contains] es un filtro de coincidencia parcial, por lo que una dirección corta puede coincidir con varios contactos.

En una tienda con mucho volumen, prefiero ajustar el resultado comparando el correo electrónico exacto del contacto devuelto antes de reutilizar su ID, lo que evita otorgar una oferta a la persona equivocada que casualmente comparte una subcadena.

El webhook se dispara, pero el acceso no se actualiza

O bien el endpoint está rechazando la llamada, o bien se está confiando ciegamente en la carga útil.

Primero, confirma que el secreto compartido X-Kajabi-Webhook-Secrettoken en la URL del hook de Kajabi guardada coincide exactamente con KAJABI_WEBHOOK_TOKEN en tu entorno desplegado; una discrepancia devuelve un 401 y el registro de entrega de Kajabi mostrará el fallo.

Si la llamada llega pero el estado es incorrecto, la causa suele ser confiar en el cuerpo del webhook en lugar de volver a consultar. Los webhooks de ofertas se envían sin una firma documentada que puedas verificar, por lo que la carga útil es una notificación, no una prueba.

Vuelve a consultar a Kajabi el estado actual de la oferta del contacto dentro del controlador y guarda ese resultado, nunca la carga útil original. Vale la pena conocer una trampa de tiempo más: la entrega del webhook y la lectura tras escritura de la API no son perfectamente instantáneas. Por lo tanto, una nueva consulta realizada en el mismo milisegundo en que llega un hook puede leer ocasionalmente un estado desactualizado.

Si observas fluctuaciones en las inscripciones, añade un reintento breve o un retraso de unos segundos antes de la lectura de verificación y deja que el estado se estabilice.

Amplía la integración de Kajabi en tu aplicación de Webflow Cloud

Los seis pasos anteriores cubren el ciclo principal: autenticar, listar, verificar, conceder y sincronizar. Una vez que este ciclo es sólido, la misma kajabiFetch utilidad abre el resto de la plataforma sin necesidad de nueva infraestructura.

Un portal para miembros es la siguiente construcción más común. Con la verificación de acceso ya en el lado del servidor, puedes extraer los módulos y lecciones de un curso usando GET /v1/courses/{id}?include=modules,lessons,lessons.media y renderizar una vista real de "mis cursos", el tipo de pantalla autenticada y basada en datos que se explica en la creación de un panel en tiempo real en Webflow Cloud.

Las miniaturas de los cursos que provienen de Kajabi son una buena opción para servir imágenes optimizadas y adaptables, de modo que un catálogo de imágenes de portada grandes no ralentice la página. Y dado que un webhook que falla silenciosamente es la forma en que la inscripción se pierde discretamente, añadir seguimiento de errores a la aplicación de Webflow Cloud alrededor del gestor de sincronización se paga solo la primera vez que Kajabi cambia una carga útil.

Explora Webflow + Kajabi para comparar los enfoques de incrustación y automatización frente a la ruta de la API.

Preguntas frecuentes

¿Funciona la API pública de Kajabi en el entorno de ejecución Workers de Webflow Cloud?

Sí. La API pública de Kajabi es HTTPS estándar con OAuth 2.0, por lo que cada llamada es una petición estándar fetch contra https://api.kajabi.com/v1. No hay ningún SDK exclusivo de Node que importar, que es exactamente lo que necesita el entorno de ejecución de Cloudflare Workers detrás de Webflow Cloud. Webflow Cloud ejecuta tus gestores de rutas en el entorno de ejecución de Node.js a través del adaptador OpenNext Cloudflare, por lo que la integración funciona sin capas de compatibilidad.

¿Necesito el plan Pro de Kajabi para usar la API?

La API pública está incluida en el nuevo plan Pro de Kajabi, anunciado el 16 de septiembre de 2025, y también está disponible como complemento de pago. Si tu cuenta es anterior a ese plan o está en un nivel inferior, consulta Ajustes > API pública para confirmar el acceso antes de empezar a construir. Los precios y los detalles de los planes cambian, así que verifica las condiciones actuales en kajabi.com/pricing.

¿Puedo integrar cursos de Kajabi sin la API pública?

Sí, pero con menos control. Sin acceso a la API, puedes incrustar formularios y botones de pago de Kajabi mediante el código incrustado personalizado de Webflow, y usar las URL de webhook de activación y desactivación de ofertas de Kajabi, a las que puedes enviar una solicitud POST desde un gestor de rutas para conceder o revocar el acceso sin la API. Pierdes el listado del catálogo y las comprobaciones de acceso en el lado del servidor, por lo que la experiencia se parece más a una visualización con redirección que a una integración real. Los webhooks están disponibles en los planes Growth y Pro, por lo que esta vía funciona sin la API en cualquiera de ellos.

¿Cómo mantengo seguro mi client_secret de Kajabi en Webflow Cloud?

Almacénalo como una variable de entorno de Webflow Cloud sin el NEXT_PUBLIC_ prefijo, y léelo solo dentro de los gestores de rutas. Nunca lo pongas en el código del cliente y nunca devuelvas el token de acceso al navegador. Si sospechas de una filtración, rota la clave en Ajustes > API pública; esto invalida inmediatamente todos los tokens emitidos con el secreto anterior.

¿Puedo sincronizar cursos de Kajabi en el CMS de Webflow?

Sí. En un gestor de rutas, obtén los cursos de Kajabi y escríbelos en una colección del CMS usando la API del CMS de Webflow, asignando el título, la descripción y la miniatura de Kajabi a los campos de tu colección. Actívalo en el gestor de webhooks para que un curso de Kajabi nuevo o modificado actualice el elemento correspondiente del CMS, lo cual es útil cuando quieres que los cursos estén indexados y diseñados como páginas nativas de Webflow en lugar de obtenerlos en el momento de la solicitud.


Última actualización
July 25, 2026
Categoría

Artículos relacionados

Cómo vincular formularios de Webflow a HubSpot sin perder el diseño de tu formulario
Cómo vincular formularios de Webflow a HubSpot sin perder el diseño de tu formulario

Cómo vincular formularios de Webflow a HubSpot sin perder el diseño de tu formulario

Cómo vincular formularios de Webflow a HubSpot sin perder el diseño de tu formulario

Desarrollo
Por
Colin Lateano
,
,
Leer artículo
Cómo personalizar los correos de notificación de formularios para cada formulario de tu sitio
Cómo personalizar los correos de notificación de formularios para cada formulario de tu sitio

Cómo personalizar los correos de notificación de formularios para cada formulario de tu sitio

Cómo personalizar los correos de notificación de formularios para cada formulario de tu sitio

Desarrollo
Por
Colin Lateano
,
,
Leer artículo
¿Cómo se añade un banner de consentimiento de cookies que cumpla con el RGPD a Webflow usando Cookiebot?
¿Cómo se añade un banner de consentimiento de cookies que cumpla con el RGPD a Webflow usando Cookiebot?

¿Cómo se añade un banner de consentimiento de cookies que cumpla con el RGPD a Webflow usando Cookiebot?

¿Cómo se añade un banner de consentimiento de cookies que cumpla con el RGPD a Webflow usando Cookiebot?

Desarrollo
Por
Colin Lateano
,
,
Leer artículo
Cómo crear una aplicación generadora de facturas en Webflow Cloud con Next.js y SQLite
Cómo crear una aplicación generadora de facturas en Webflow Cloud con Next.js y SQLite

Cómo crear una aplicación generadora de facturas en Webflow Cloud con Next.js y SQLite

Cómo crear una aplicación generadora de facturas en Webflow Cloud con Next.js y SQLite

Guías
Por
Ismail Ajagbe
,
,
Leer artículo

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.