// Server-only — syncs newsletter opt-ins to Brevo's Contacts API. Brevo // owns everything downstream of that (list membership, unsubscribe links, // and whatever Welcome Flow automation is configured on the list in // Brevo's own UI — that automation isn't manageable via their public API // at all, only contacts/lists are). This app never sends marketing mail // itself; it only ever hands Brevo the contact + consent. const BREVO_API_URL = "https://api.brevo.com/v3/contacts"; export type BrevoSyncResult = { ok: true } | { ok: false; reason: string }; export type NewsletterOptInSource = "checkout" | "newsletter-page" | "newsletter-modal" | "newsletter-hero" | "challenge"; // `source` becomes a Brevo contact attribute so campaigns/segments can // tell a checkout opt-in apart from the standalone signup forms without // needing separate lists. export async function upsertNewsletterContact( email: string, source: NewsletterOptInSource, ): Promise { const apiKey = process.env.BREVO_API_KEY; const listId = process.env.BREVO_LIST_ID; if (!apiKey || !listId) { return { ok: false, reason: "BREVO_API_KEY/BREVO_LIST_ID nicht konfiguriert." }; } try { const res = await fetch(BREVO_API_URL, { method: "POST", headers: { "Content-Type": "application/json", "api-key": apiKey, }, body: JSON.stringify({ email, listIds: [Number(listId)], updateEnabled: true, attributes: { OPT_IN_SOURCE: source }, }), signal: AbortSignal.timeout(8000), }); // 204 for both a fresh contact and an existing one (updateEnabled // above merges the list membership onto the existing contact instead // of erroring). if (res.ok || res.status === 204) return { ok: true }; const body = await res.json().catch(() => null); return { ok: false, reason: body?.message ?? `Brevo antwortete mit ${res.status}` }; } catch (err) { return { ok: false, reason: err instanceof Error ? err.message : "Brevo ist gerade nicht erreichbar." }; } }