Subscription hooks
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.
| Hook | Purpose | Failure behavior |
|---|---|---|
listmonk:subscribe:before | Run guards after subscriber validation and immediately before Listmonk | Blocks the request and preserves the thrown error |
listmonk:subscribe:after | Observe a completed subscription and its response | Logs the hook failure without changing the successful response |
listmonk:subscribe:error | Observe post-validation guard, configuration, or Listmonk failures | Logs 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:
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:
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:
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.