API

Subscription hooks

The subscription endpoint exposes provider-agnostic Nitro hooks for security policies and observability. Applications can combine rate limiting, CAPTCHA, CSRF checks, honeypots, allowlists, or custom validation without adding those providers to nuxt-listmonk.

Subscription hooks

The subscription endpoint exposes provider-agnostic Nitro hooks for security policies and observability. Applications can combine rate limiting, CAPTCHA, CSRF checks, honeypots, allowlists, or custom validation without adding those providers to nuxt-listmonk.

HookPurposeFailure behavior
listmonk:subscribe:beforeRun guards after subscriber validation and immediately before ListmonkBlocks the request and preserves the thrown error
listmonk:subscribe:afterObserve a completed subscription and its responseLogs the hook failure without changing the successful response
listmonk:subscribe:errorObserve post-validation guard, configuration, or Listmonk failuresLogs the hook failure without replacing the original error

Hooks are awaited and run serially in registration order. If one hook throws, later callbacks for the same hook name do not run. When no callbacks are registered, subscription behavior is unchanged.

Protect subscriptions

Register guards in a server-only Nitro plugin. Throw an H3 error when the request should be rejected:

server/plugins/newsletter-guard.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook(
    'listmonk:subscribe:before',
    async ({ event, body, subscriber }) => {
      await enforceNewsletterRateLimit(event)

      const token = body.recaptchaToken

      if (typeof token !== 'string' || !token) {
        throw createError({
          statusCode: 400,
          statusMessage: 'Missing CAPTCHA token.',
        })
      }

      await verifyRecaptcha({
        event,
        token,
        email: subscriber.email,
      })
    },
  )
})

enforceNewsletterRateLimit() and verifyRecaptcha() belong to the consuming application. Keep provider secrets in private server runtime configuration. The module reads the body only once and passes the parsed body to the hook. Custom fields such as recaptchaToken never reach Listmonk.

Multiple policies can be registered independently:

server/plugins/newsletter-honeypot.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('listmonk:subscribe:before', ({ body }) => {
    if (body.website) {
      throw createError({ statusCode: 400 })
    }
  })
})

Observe outcomes

The after hook receives the validated context and the exact response returned to the client:

server/plugins/newsletter-monitoring.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook(
    'listmonk:subscribe:after',
    async ({ subscriber, response }) => {
      await recordNewsletterSubscription({
        email: subscriber.email,
        message: response.message,
      })
    },
  )

  nitroApp.hooks.hook(
    'listmonk:subscribe:error',
    async ({ event, subscriber, stage, error }) => {
      await recordNewsletterFailure({
        requestPath: getRequestPath(event),
        email: subscriber.email,
        stage,
        statusCode: error.statusCode,
        statusMessage: error.statusMessage,
      })
    },
  )
})

The error stages are before, configuration, and listmonk. The error hook runs only after the subscriber has been validated; body-reading and validation errors do not emit it. Its context omits the raw request body, original error, stack, cause, custom tokens, and Listmonk credentials.

The after context does contain the original body for application-owned observers. Avoid logging it because it may contain CAPTCHA tokens or other sensitive fields. Failures from after do not emit the error hook.

Copyright © 2026