Formularios web: ejemplo con Next.js

Última actualización: Oct 12, 2026Sección: Integraciones

Integra un formulario de suscripción en Next.js con una ruta BFF: el navegador envía a tu aplicación; tu ruta llama a Mailoo con credenciales del servidor. Prefiere @mailoo/forms. Para el contrato de la API, consulta website-forms{.interpreted-text role="doc"}.

Requisitos previos

  • Un proyecto de Mailoo con una integración de tipo Form (FORM)
  • Una clave API con webhook.form-submission (o FULL)

Entorno (solo servidor)

@mailoo/forms usa por defecto el prefijo de entorno MAILOO_CONTACT_FORM_INTEGRATION para integraciones FORM. El nombre es histórico: no es la integración CONTACT_FORM (comentarios). Los comentarios usan MAILOO_FEEDBACK_INTEGRATION --- consulta feedback-form-nextjs-example{.interpreted-text role="doc"}.

Añade a .env.local (o tus secretos del host):

MAILOO_CONTACT_FORM_INTEGRATION_API=https://api.mailoo.app
MAILOO_CONTACT_FORM_INTEGRATION_API_KEY=your-api-key-here
MAILOO_CONTACT_FORM_INTEGRATION_PROJECT_UID=your-project-uid-here
MAILOO_CONTACT_FORM_INTEGRATION_ID=your-form-integration-id-here

Las cuatro son obligatorias para el prefijo por defecto. No uses NEXT_PUBLIC_* para la URL de la API ni la clave. Para una segunda integración FORM, pasa un prefix personalizado (o getConfig) a la fábrica de rutas.

Controlador de ruta BFF

// app/api/v1/webhooks/forms/submit/route.ts
import { createFormSubmitHandler } from '@mailoo/forms/routes'

export const POST = createFormSubmitHandler()

Formulario del cliente

Envía a tu BFF. Campos del cuerpo para FORM: email (obligatorio), name, subject, content, source, metadata opcionales. Usa content (no message) si quieres un cuerpo personalizado en el buzón.

Con el hook del paquete:

'use client'

import { useState } from 'react'
import { useMailooFormSubmit } from '@mailoo/forms/hooks'

export function NewsletterForm() {
  const { submit, pending } = useMailooFormSubmit({
    endpoint: '/api/v1/webhooks/forms/submit',
  })
  const [statusMessage, setStatusMessage] = useState('')

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    setStatusMessage('')
    const fd = new FormData(e.currentTarget)
    const result = await submit({
      email: String(fd.get('email') || '').trim(),
      name: String(fd.get('name') || '').trim() || undefined,
      source: typeof window !== 'undefined' ? window.location.href : '',
    })
    if (!result.ok) {
      setStatusMessage(result.message)
      return
    }
    const data = result.data as { message?: string }
    setStatusMessage(
      typeof data.message === 'string' ? data.message : 'Subscribed'
    )
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="name" placeholder="Your name (optional)" disabled={pending} />
      <input name="email" type="email" required disabled={pending} />
      <button type="submit" disabled={pending}>
        {pending ? 'Subscribing…' : 'Subscribe'}
      </button>
      {statusMessage ? <p>{statusMessage}</p> : null}
    </form>
  )
}

O llama a fetch tú mismo con la misma estructura JSON.

Eventos de ciclo de vida

El envío anónimo de boletín es distinto del registro de un usuario identificado. Después de crear el usuario en tu aplicación, emite desde el servidor:

import { ingestMailooLifecycleEvent } from '@mailoo/forms/server'
import { MAILOO_LIFECYCLE_EVENTS } from '@mailoo/forms/events'

await ingestMailooLifecycleEvent({
  eventName: MAILOO_LIFECYCLE_EVENTS.USER_REGISTERED,
  idempotencyKey: `reg:${user.id}`,
  externalUserId: user.id,
  email: user.email,
  marketingConsent: { granted: true, source: 'signup' },
})

Pasa el id de usuario de tu app como externalUserId (no el contactId de Mailoo). Browser → BFF: useMailooLifecycleEvent({ getHeaders }) para Authorization del host. Estado admin en lote: useMailooLifecycleStatus → ?externalUserIds=. Retry: POST .../events/{eventId}/retry vía useMailooLifecycleRetry.

Prefijo de entorno por defecto: MAILOO_LIFECYCLE_*. Contrato completo: lifecycle-events{.interpreted-text role="doc"}.

Relacionado

  • website-forms{.interpreted-text role="doc"} --- endpoint y comportamiento de suscriptores
  • lifecycle-events{.interpreted-text role="doc"} --- user.registered / user.activated
  • feedback-form-nextjs-example{.interpreted-text role="doc"} --- CONTACT_FORM / comentarios
  • nextjs-packages{.interpreted-text role="doc"} --- resumen de paquetes