[{"data":1,"prerenderedAt":949},["ShallowReactive",2],{"navigation_docs":3,"-api-subscription-hooks":51,"-api-subscription-hooks-surround":946},[4,15,37],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":14},"Introduction","i-lucide-star","\u002Fintroduction","1.introduction",[10],{"title":11,"path":12,"stem":13},"Getting Started","\u002Fintroduction\u002Fgetting-started","1.introduction\u002F1.getting-started",false,{"title":16,"icon":17,"path":18,"stem":19,"children":20,"page":14},"Components","i-lucide-box","\u002Fcomponents","2.components",[21,25,29,33],{"title":22,"path":23,"stem":24},"ListmonkForm","\u002Fcomponents\u002Flistmonkform","2.components\u002F1.ListmonkForm",{"title":26,"path":27,"stem":28},"ListmonkInputGroup","\u002Fcomponents\u002Flistmonkinputgroup","2.components\u002F2.ListmonkInputGroup",{"title":30,"path":31,"stem":32},"ListmonkInput","\u002Fcomponents\u002Flistmonkinput","2.components\u002F3.ListmonkInput",{"title":34,"path":35,"stem":36},"ListmonkButton","\u002Fcomponents\u002Flistmonkbutton","2.components\u002F4.ListmonkButton",{"title":38,"icon":39,"path":40,"stem":41,"children":42,"page":14},"API","i-lucide-bookmark","\u002Fapi","3.api",[43,47],{"title":44,"path":45,"stem":46},"Composables","\u002Fapi\u002Fcomposables","3.api\u002F1.composables",{"title":48,"path":49,"stem":50},"Subscription hooks","\u002Fapi\u002Fsubscription-hooks","3.api\u002F2.subscription-hooks",{"id":52,"title":48,"body":53,"description":63,"extension":941,"links":942,"meta":943,"navigation":273,"path":49,"seo":944,"stem":50,"__hash__":945},"docs\u002F3.api\u002F2.subscription-hooks.md",{"type":54,"value":55,"toc":937},"minimark",[56,60,64,127,130,135,138,476,490,493,612,616,623,906,921,933],[57,58,48],"h1",{"id":59},"subscription-hooks",[61,62,63],"p",{},"The subscription endpoint exposes provider-agnostic Nitro hooks for security\npolicies and observability. Applications can combine rate limiting, CAPTCHA,\nCSRF checks, honeypots, allowlists, or custom validation without adding those\nproviders to nuxt-listmonk.",[65,66,67,83],"table",{},[68,69,70],"thead",{},[71,72,73,77,80],"tr",{},[74,75,76],"th",{},"Hook",[74,78,79],{},"Purpose",[74,81,82],{},"Failure behavior",[84,85,86,101,114],"tbody",{},[71,87,88,95,98],{},[89,90,91],"td",{},[92,93,94],"code",{},"listmonk:subscribe:before",[89,96,97],{},"Run guards after subscriber validation and immediately before Listmonk",[89,99,100],{},"Blocks the request and preserves the thrown error",[71,102,103,108,111],{},[89,104,105],{},[92,106,107],{},"listmonk:subscribe:after",[89,109,110],{},"Observe a completed subscription and its response",[89,112,113],{},"Logs the hook failure without changing the successful response",[71,115,116,121,124],{},[89,117,118],{},[92,119,120],{},"listmonk:subscribe:error",[89,122,123],{},"Observe post-validation guard, configuration, or Listmonk failures",[89,125,126],{},"Logs the hook failure without replacing the original error",[61,128,129],{},"Hooks are awaited and run serially in registration order. If one hook throws,\nlater callbacks for the same hook name do not run. When no callbacks are\nregistered, subscription behavior is unchanged.",[131,132,134],"h2",{"id":133},"protect-subscriptions","Protect subscriptions",[61,136,137],{},"Register guards in a server-only Nitro plugin. Throw an H3 error when the\nrequest should be rejected:",[139,140,146],"pre",{"className":141,"code":142,"filename":143,"language":144,"meta":145,"style":145},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook(\n    'listmonk:subscribe:before',\n    async ({ event, body, subscriber }) => {\n      await enforceNewsletterRateLimit(event)\n\n      const token = body.recaptchaToken\n\n      if (typeof token !== 'string' || !token) {\n        throw createError({\n          statusCode: 400,\n          statusMessage: 'Missing CAPTCHA token.',\n        })\n      }\n\n      await verifyRecaptcha({\n        event,\n        token,\n        email: subscriber.email,\n      })\n    },\n  )\n})\n","server\u002Fplugins\u002Fnewsletter-guard.ts","ts","",[92,147,148,185,206,221,251,268,275,294,299,339,352,367,384,392,398,403,415,423,431,448,456,462,468],{"__ignoreMap":145},[149,150,153,157,160,164,168,171,175,178,182],"span",{"class":151,"line":152},"line",1,[149,154,156],{"class":155},"s7zQu","export",[149,158,159],{"class":155}," default",[149,161,163],{"class":162},"s2Zo4"," defineNitroPlugin",[149,165,167],{"class":166},"sTEyZ","(",[149,169,167],{"class":170},"sMK4o",[149,172,174],{"class":173},"sHdIc","nitroApp",[149,176,177],{"class":170},")",[149,179,181],{"class":180},"spNyl"," =>",[149,183,184],{"class":170}," {\n",[149,186,188,191,194,197,199,202],{"class":151,"line":187},2,[149,189,190],{"class":166},"  nitroApp",[149,192,193],{"class":170},".",[149,195,196],{"class":166},"hooks",[149,198,193],{"class":170},[149,200,201],{"class":162},"hook",[149,203,205],{"class":204},"swJcz","(\n",[149,207,209,212,215,218],{"class":151,"line":208},3,[149,210,211],{"class":170},"    '",[149,213,94],{"class":214},"sfazB",[149,216,217],{"class":170},"'",[149,219,220],{"class":170},",\n",[149,222,224,227,230,233,236,239,241,244,247,249],{"class":151,"line":223},4,[149,225,226],{"class":180},"    async",[149,228,229],{"class":170}," ({",[149,231,232],{"class":173}," event",[149,234,235],{"class":170},",",[149,237,238],{"class":173}," body",[149,240,235],{"class":170},[149,242,243],{"class":173}," subscriber",[149,245,246],{"class":170}," })",[149,248,181],{"class":180},[149,250,184],{"class":170},[149,252,254,257,260,262,265],{"class":151,"line":253},5,[149,255,256],{"class":155},"      await",[149,258,259],{"class":162}," enforceNewsletterRateLimit",[149,261,167],{"class":204},[149,263,264],{"class":166},"event",[149,266,267],{"class":204},")\n",[149,269,271],{"class":151,"line":270},6,[149,272,274],{"emptyLinePlaceholder":273},true,"\n",[149,276,278,281,284,287,289,291],{"class":151,"line":277},7,[149,279,280],{"class":180},"      const",[149,282,283],{"class":166}," token",[149,285,286],{"class":170}," =",[149,288,238],{"class":166},[149,290,193],{"class":170},[149,292,293],{"class":166},"recaptchaToken\n",[149,295,297],{"class":151,"line":296},8,[149,298,274],{"emptyLinePlaceholder":273},[149,300,302,305,308,311,313,316,319,322,324,327,330,333,336],{"class":151,"line":301},9,[149,303,304],{"class":155},"      if",[149,306,307],{"class":204}," (",[149,309,310],{"class":170},"typeof",[149,312,283],{"class":166},[149,314,315],{"class":170}," !==",[149,317,318],{"class":170}," '",[149,320,321],{"class":214},"string",[149,323,217],{"class":170},[149,325,326],{"class":170}," ||",[149,328,329],{"class":170}," !",[149,331,332],{"class":166},"token",[149,334,335],{"class":204},") ",[149,337,338],{"class":170},"{\n",[149,340,342,345,348,350],{"class":151,"line":341},10,[149,343,344],{"class":155},"        throw",[149,346,347],{"class":162}," createError",[149,349,167],{"class":204},[149,351,338],{"class":170},[149,353,355,358,361,365],{"class":151,"line":354},11,[149,356,357],{"class":204},"          statusCode",[149,359,360],{"class":170},":",[149,362,364],{"class":363},"sbssI"," 400",[149,366,220],{"class":170},[149,368,370,373,375,377,380,382],{"class":151,"line":369},12,[149,371,372],{"class":204},"          statusMessage",[149,374,360],{"class":170},[149,376,318],{"class":170},[149,378,379],{"class":214},"Missing CAPTCHA token.",[149,381,217],{"class":170},[149,383,220],{"class":170},[149,385,387,390],{"class":151,"line":386},13,[149,388,389],{"class":170},"        }",[149,391,267],{"class":204},[149,393,395],{"class":151,"line":394},14,[149,396,397],{"class":170},"      }\n",[149,399,401],{"class":151,"line":400},15,[149,402,274],{"emptyLinePlaceholder":273},[149,404,406,408,411,413],{"class":151,"line":405},16,[149,407,256],{"class":155},[149,409,410],{"class":162}," verifyRecaptcha",[149,412,167],{"class":204},[149,414,338],{"class":170},[149,416,418,421],{"class":151,"line":417},17,[149,419,420],{"class":166},"        event",[149,422,220],{"class":170},[149,424,426,429],{"class":151,"line":425},18,[149,427,428],{"class":166},"        token",[149,430,220],{"class":170},[149,432,434,437,439,441,443,446],{"class":151,"line":433},19,[149,435,436],{"class":204},"        email",[149,438,360],{"class":170},[149,440,243],{"class":166},[149,442,193],{"class":170},[149,444,445],{"class":166},"email",[149,447,220],{"class":170},[149,449,451,454],{"class":151,"line":450},20,[149,452,453],{"class":170},"      }",[149,455,267],{"class":204},[149,457,459],{"class":151,"line":458},21,[149,460,461],{"class":170},"    },\n",[149,463,465],{"class":151,"line":464},22,[149,466,467],{"class":204},"  )\n",[149,469,471,474],{"class":151,"line":470},23,[149,472,473],{"class":170},"}",[149,475,267],{"class":166},[61,477,478,481,482,485,486,489],{},[92,479,480],{},"enforceNewsletterRateLimit()"," and ",[92,483,484],{},"verifyRecaptcha()"," belong to the consuming\napplication. Keep provider secrets in private server runtime configuration.\nThe module reads the body only once and passes the parsed body to the hook.\nCustom fields such as ",[92,487,488],{},"recaptchaToken"," never reach Listmonk.",[61,491,492],{},"Multiple policies can be registered independently:",[139,494,497],{"className":141,"code":495,"filename":496,"language":144,"meta":145,"style":145},"export default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook('listmonk:subscribe:before', ({ body }) => {\n    if (body.website) {\n      throw createError({ statusCode: 400 })\n    }\n  })\n})\n","server\u002Fplugins\u002Fnewsletter-honeypot.ts",[92,498,499,519,551,570,594,599,606],{"__ignoreMap":145},[149,500,501,503,505,507,509,511,513,515,517],{"class":151,"line":152},[149,502,156],{"class":155},[149,504,159],{"class":155},[149,506,163],{"class":162},[149,508,167],{"class":166},[149,510,167],{"class":170},[149,512,174],{"class":173},[149,514,177],{"class":170},[149,516,181],{"class":180},[149,518,184],{"class":170},[149,520,521,523,525,527,529,531,533,535,537,539,541,543,545,547,549],{"class":151,"line":187},[149,522,190],{"class":166},[149,524,193],{"class":170},[149,526,196],{"class":166},[149,528,193],{"class":170},[149,530,201],{"class":162},[149,532,167],{"class":204},[149,534,217],{"class":170},[149,536,94],{"class":214},[149,538,217],{"class":170},[149,540,235],{"class":170},[149,542,229],{"class":170},[149,544,238],{"class":173},[149,546,246],{"class":170},[149,548,181],{"class":180},[149,550,184],{"class":170},[149,552,553,556,558,561,563,566,568],{"class":151,"line":208},[149,554,555],{"class":155},"    if",[149,557,307],{"class":204},[149,559,560],{"class":166},"body",[149,562,193],{"class":170},[149,564,565],{"class":166},"website",[149,567,335],{"class":204},[149,569,338],{"class":170},[149,571,572,575,577,579,582,585,587,589,592],{"class":151,"line":223},[149,573,574],{"class":155},"      throw",[149,576,347],{"class":162},[149,578,167],{"class":204},[149,580,581],{"class":170},"{",[149,583,584],{"class":204}," statusCode",[149,586,360],{"class":170},[149,588,364],{"class":363},[149,590,591],{"class":170}," }",[149,593,267],{"class":204},[149,595,596],{"class":151,"line":253},[149,597,598],{"class":170},"    }\n",[149,600,601,604],{"class":151,"line":270},[149,602,603],{"class":170},"  }",[149,605,267],{"class":204},[149,607,608,610],{"class":151,"line":277},[149,609,473],{"class":170},[149,611,267],{"class":166},[131,613,615],{"id":614},"observe-outcomes","Observe outcomes",[61,617,618,619,622],{},"The ",[92,620,621],{},"after"," hook receives the validated context and the exact response returned\nto the client:",[139,624,627],{"className":141,"code":625,"filename":626,"language":144,"meta":145,"style":145},"export default defineNitroPlugin((nitroApp) => {\n  nitroApp.hooks.hook(\n    'listmonk:subscribe:after',\n    async ({ subscriber, response }) => {\n      await recordNewsletterSubscription({\n        email: subscriber.email,\n        message: response.message,\n      })\n    },\n  )\n\n  nitroApp.hooks.hook(\n    'listmonk:subscribe:error',\n    async ({ event, subscriber, stage, error }) => {\n      await recordNewsletterFailure({\n        requestPath: getRequestPath(event),\n        email: subscriber.email,\n        stage,\n        statusCode: error.statusCode,\n        statusMessage: error.statusMessage,\n      })\n    },\n  )\n})\n","server\u002Fplugins\u002Fnewsletter-monitoring.ts",[92,628,629,649,663,673,692,703,717,733,739,743,747,751,765,775,803,814,832,846,853,869,885,891,895,899],{"__ignoreMap":145},[149,630,631,633,635,637,639,641,643,645,647],{"class":151,"line":152},[149,632,156],{"class":155},[149,634,159],{"class":155},[149,636,163],{"class":162},[149,638,167],{"class":166},[149,640,167],{"class":170},[149,642,174],{"class":173},[149,644,177],{"class":170},[149,646,181],{"class":180},[149,648,184],{"class":170},[149,650,651,653,655,657,659,661],{"class":151,"line":187},[149,652,190],{"class":166},[149,654,193],{"class":170},[149,656,196],{"class":166},[149,658,193],{"class":170},[149,660,201],{"class":162},[149,662,205],{"class":204},[149,664,665,667,669,671],{"class":151,"line":208},[149,666,211],{"class":170},[149,668,107],{"class":214},[149,670,217],{"class":170},[149,672,220],{"class":170},[149,674,675,677,679,681,683,686,688,690],{"class":151,"line":223},[149,676,226],{"class":180},[149,678,229],{"class":170},[149,680,243],{"class":173},[149,682,235],{"class":170},[149,684,685],{"class":173}," response",[149,687,246],{"class":170},[149,689,181],{"class":180},[149,691,184],{"class":170},[149,693,694,696,699,701],{"class":151,"line":253},[149,695,256],{"class":155},[149,697,698],{"class":162}," recordNewsletterSubscription",[149,700,167],{"class":204},[149,702,338],{"class":170},[149,704,705,707,709,711,713,715],{"class":151,"line":270},[149,706,436],{"class":204},[149,708,360],{"class":170},[149,710,243],{"class":166},[149,712,193],{"class":170},[149,714,445],{"class":166},[149,716,220],{"class":170},[149,718,719,722,724,726,728,731],{"class":151,"line":277},[149,720,721],{"class":204},"        message",[149,723,360],{"class":170},[149,725,685],{"class":166},[149,727,193],{"class":170},[149,729,730],{"class":166},"message",[149,732,220],{"class":170},[149,734,735,737],{"class":151,"line":296},[149,736,453],{"class":170},[149,738,267],{"class":204},[149,740,741],{"class":151,"line":301},[149,742,461],{"class":170},[149,744,745],{"class":151,"line":341},[149,746,467],{"class":204},[149,748,749],{"class":151,"line":354},[149,750,274],{"emptyLinePlaceholder":273},[149,752,753,755,757,759,761,763],{"class":151,"line":369},[149,754,190],{"class":166},[149,756,193],{"class":170},[149,758,196],{"class":166},[149,760,193],{"class":170},[149,762,201],{"class":162},[149,764,205],{"class":204},[149,766,767,769,771,773],{"class":151,"line":386},[149,768,211],{"class":170},[149,770,120],{"class":214},[149,772,217],{"class":170},[149,774,220],{"class":170},[149,776,777,779,781,783,785,787,789,792,794,797,799,801],{"class":151,"line":394},[149,778,226],{"class":180},[149,780,229],{"class":170},[149,782,232],{"class":173},[149,784,235],{"class":170},[149,786,243],{"class":173},[149,788,235],{"class":170},[149,790,791],{"class":173}," stage",[149,793,235],{"class":170},[149,795,796],{"class":173}," error",[149,798,246],{"class":170},[149,800,181],{"class":180},[149,802,184],{"class":170},[149,804,805,807,810,812],{"class":151,"line":400},[149,806,256],{"class":155},[149,808,809],{"class":162}," recordNewsletterFailure",[149,811,167],{"class":204},[149,813,338],{"class":170},[149,815,816,819,821,824,826,828,830],{"class":151,"line":405},[149,817,818],{"class":204},"        requestPath",[149,820,360],{"class":170},[149,822,823],{"class":162}," getRequestPath",[149,825,167],{"class":204},[149,827,264],{"class":166},[149,829,177],{"class":204},[149,831,220],{"class":170},[149,833,834,836,838,840,842,844],{"class":151,"line":417},[149,835,436],{"class":204},[149,837,360],{"class":170},[149,839,243],{"class":166},[149,841,193],{"class":170},[149,843,445],{"class":166},[149,845,220],{"class":170},[149,847,848,851],{"class":151,"line":425},[149,849,850],{"class":166},"        stage",[149,852,220],{"class":170},[149,854,855,858,860,862,864,867],{"class":151,"line":433},[149,856,857],{"class":204},"        statusCode",[149,859,360],{"class":170},[149,861,796],{"class":166},[149,863,193],{"class":170},[149,865,866],{"class":166},"statusCode",[149,868,220],{"class":170},[149,870,871,874,876,878,880,883],{"class":151,"line":450},[149,872,873],{"class":204},"        statusMessage",[149,875,360],{"class":170},[149,877,796],{"class":166},[149,879,193],{"class":170},[149,881,882],{"class":166},"statusMessage",[149,884,220],{"class":170},[149,886,887,889],{"class":151,"line":458},[149,888,453],{"class":170},[149,890,267],{"class":204},[149,892,893],{"class":151,"line":464},[149,894,461],{"class":170},[149,896,897],{"class":151,"line":470},[149,898,467],{"class":204},[149,900,902,904],{"class":151,"line":901},24,[149,903,473],{"class":170},[149,905,267],{"class":166},[61,907,908,909,912,913,916,917,920],{},"The error stages are ",[92,910,911],{},"before",", ",[92,914,915],{},"configuration",", and ",[92,918,919],{},"listmonk",". The error hook\nruns only after the subscriber has been validated; body-reading and validation\nerrors do not emit it. Its context omits the raw request body, original error,\nstack, cause, custom tokens, and Listmonk credentials.",[61,922,618,923,925,926,928,929,932],{},[92,924,621],{}," context does contain the original body for application-owned\nobservers. Avoid logging it because it may contain CAPTCHA tokens or other\nsensitive fields. Failures from ",[92,927,621],{}," do not emit the ",[92,930,931],{},"error"," hook.",[934,935,936],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":145,"searchDepth":187,"depth":187,"links":938},[939,940],{"id":133,"depth":187,"text":134},{"id":614,"depth":187,"text":615},"md",null,{},{"title":48,"description":63},"VxaiNfa3jhqiVuLflR-DZvIH-6BY777bTJXfK3zbJXs",[947,942],{"title":44,"path":45,"stem":46,"description":948,"children":-1},"Discover the composables to use in your custom Vue components and pages.",1788751973688]