import { NextResponse } from "next/server"; const ARCHETYPES = ["architect", "gardener", "librarian", "pragmatist"] as const; type Archetype = (typeof ARCHETYPES)[number]; interface ListmonkSubscriber { id: number; } interface ListmonkSearchResult { data?: { results?: ListmonkSubscriber[]; }; } function buildAuth(): string { return ( "Basic " + Buffer.from( `${process.env.LISTMONK_USER}:${process.env.LISTMONK_TOKEN}`, ).toString("base64") ); } function listmonkUrl(path: string): string { return `${process.env.LISTMONK_URL}${path}`; } function getListId(): number { return Number(process.env.LISTMONK_QUIZ_LIST_ID); } function isValidEmail(email: string): boolean { return /^[^@]+@[^@]+\.[^@]+$/.test(email); } function isValidArchetype(value: string): value is Archetype { return (ARCHETYPES as readonly string[]).includes(value); } /** * POST /api/internal/quiz-lead * * Receives a completed Typebot quiz lead and idempotently upserts * the subscriber in Listmonk with archetype + score attributes. * * Auth: x-quiz-secret header must match QUIZ_LEAD_SECRET env var. */ export async function POST(req: Request): Promise { // --- Auth --- const secret = req.headers.get("x-quiz-secret"); if (!secret || secret !== process.env.QUIZ_LEAD_SECRET) { return NextResponse.json({ error: "unauthorized" }, { status: 401 }); } // --- Parse body --- let body: unknown; try { body = await req.json(); } catch { return NextResponse.json({ error: "bad json" }, { status: 400 }); } // --- Validate fields --- const raw = body as Record; const email = String(raw.email ?? "") .trim() .toLowerCase(); const archetype = String(raw.archetype ?? ""); const score = Number(raw.score); if (!isValidEmail(email) || !isValidArchetype(archetype) || !Number.isFinite(score)) { return NextResponse.json({ error: "invalid fields" }, { status: 400 }); } const attribs = { archetype, score, utm_source: raw.utm_source != null ? String(raw.utm_source) : null, utm_medium: raw.utm_medium != null ? String(raw.utm_medium) : null, utm_campaign: raw.utm_campaign != null ? String(raw.utm_campaign) : null, }; const auth = buildAuth(); const listId = getListId(); // --- Upsert to Listmonk --- try { const createRes = await fetch(listmonkUrl("/api/subscribers"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: auth }, body: JSON.stringify({ email, name: email.split("@")[0], lists: [listId], attribs, status: "enabled", preconfirm_subscriptions: true, }), }); if (createRes.status === 409) { // Subscriber exists — look up by email and update const searchRes = await fetch( listmonkUrl("/api/subscribers?query=") + encodeURIComponent(`subscribers.email='${email}'`), { headers: { Authorization: auth } }, ); const searchJson = (await searchRes.json()) as ListmonkSearchResult; const existingId = searchJson?.data?.results?.[0]?.id; if (!existingId) { console.error("quiz-lead: subscriber lookup failed for", email); return NextResponse.json({ error: "lookup failed" }, { status: 502 }); } const updateRes = await fetch(listmonkUrl(`/api/subscribers/${existingId}`), { method: "PUT", headers: { "Content-Type": "application/json", Authorization: auth }, body: JSON.stringify({ email, name: email.split("@")[0], lists: [listId], attribs, status: "enabled", }), }); if (!updateRes.ok) { console.error( "quiz-lead: subscriber update failed", updateRes.status, await updateRes.text(), ); return NextResponse.json({ error: "listmonk" }, { status: 502 }); } } else if (!createRes.ok) { console.error( "quiz-lead: listmonk create failed", createRes.status, await createRes.text(), ); return NextResponse.json({ error: "listmonk" }, { status: 502 }); } } catch (e) { console.error("quiz-lead: upstream error", e); return NextResponse.json({ error: "upstream" }, { status: 502 }); } return NextResponse.json({ ok: true }); }