// Config.gs // // ============================================================================ // SETUP — this is the only file you need to edit to point this template at // your own Klaviyo account. Full walkthrough in SETUP.md. // ============================================================================ // // 1) API KEY — do NOT paste it below. Instead: // Extensions > Apps Script > Project Settings > Script Properties // Add property: KLAVIYO_API_KEY = // The key needs Read access to: Campaigns, Flows, Metrics, Profiles, // Lists, and Segments. (Lists/Segments are used to resolve campaign // audience names, not just for SMS tracking.) // // 2) Finding metric and list IDs — easiest way: after adding your API key, // run klaviyoLogAccountInfo() (in Klaviyo.gs) from the Apps Script editor. // It logs every metric and list in your account with its ID. // (Manual alternative: Klaviyo dashboard > Analytics > Metrics > click a // metric > the ID is in the URL. Same pattern for list pages.) // // 3) Fill in CONFIG below. Every value marked REPLACE_ME needs a real value // from your own Klaviyo account before this will run. // // ============================================================================ const KLAVIYO_BASE = "https://a.klaviyo.com/api"; const KLAVIYO_REVISION = "2024-10-15"; const KLAVIYO_API_KEY_PROP = "KLAVIYO_API_KEY"; const CONFIG = { // Klaviyo's campaign/flow report APIs require a conversion_metric_id even // for the base engagement stats (opens, clicks, etc.) — this is just an // API-shape requirement, not something you need to think hard about. // Any real metric ID in your account works. "Placed Order" or // "Received Email" are safe defaults if you're not sure. BASE_METRIC_ID: "REPLACE_ME", // Add one entry per conversion metric you want tracked. Each becomes its // own "Unique {label}" column in Raw_Campaign and Raw_Flow. Track whatever // matters to your business — orders, signups, cancellations, bookings. // Leave the array empty to skip conversion tracking entirely and just get // engagement stats. // // Example: // { label: "Order Completed", metricId: "AbC123" }, // { label: "Subscription Started", metricId: "XyZ789" }, CONVERSION_METRICS: [ // { label: "REPLACE_ME", metricId: "REPLACE_ME" }, ], // Optional — SMS growth tracking (Raw_SMS_Growth sheet). // Leave SMS_LIST_ID blank ("") to skip the SMS sheet entirely. // SMS_UNSUBSCRIBE_METRIC_ID is optional on top of that: without it you // still get daily subscribes, and the unsubscribe column reports 0. // Klaviyo's built-in metric is named "Unsubscribed from SMS". SMS_LIST_ID: "", SMS_UNSUBSCRIBE_METRIC_ID: "", // IANA timezone used to bucket days in every sheet. TIMEZONE: "America/Los_Angeles", // Rolling window (days) for the scheduled pull. LOOKBACK_DAYS: 14, // Sheet tab names — change only if you want different tab names. SHEET_CAMPAIGN: "Raw_Campaign", SHEET_FLOW: "Raw_Flow", SHEET_EMAIL_GROWTH: "Raw_Email_Growth", SHEET_SMS_GROWTH: "Raw_SMS_Growth" }; // ============================================================================ // HTTP plumbing — rate-limit handling, retries. You shouldn't need to touch // anything below this line. // ============================================================================ function klaviyoFetch_(url, options = {}, maxAttempts = 6) { const apiKey = PropertiesService.getScriptProperties().getProperty(KLAVIYO_API_KEY_PROP); if (!apiKey) throw new Error(`Missing Script Property ${KLAVIYO_API_KEY_PROP}`); const headers = Object.assign( { "Authorization": `Klaviyo-API-Key ${apiKey}`, "accept": "application/json", "revision": KLAVIYO_REVISION }, options.headers || {} ); let sleepMs = 1500; for (let attempt = 1; attempt <= maxAttempts; attempt++) { // Prevent GET requests from sending JSON bodies const method = String(options.method || "get").toLowerCase(); const cleanOptions = Object.assign({}, options); if (method === "get") { delete cleanOptions.payload; delete cleanOptions.contentType; } let resp; try { resp = UrlFetchApp.fetch(url, Object.assign({}, cleanOptions, { headers, muteHttpExceptions: true })); } catch (networkErr) { // Network-level failure (DNS, address unavailable, timeout, etc.) if (attempt < maxAttempts) { Utilities.sleep(sleepMs); sleepMs = Math.min(sleepMs * 2, 15000); continue; } throw new Error(`Klaviyo network error after ${maxAttempts} attempts: ${networkErr.message}`); } const code = resp.getResponseCode(); const text = resp.getContentText(); if (code < 400) return { code, text, json: safeJson_(text) }; if (code === 429 && attempt < maxAttempts) { let waitMs = sleepMs; try { const j = JSON.parse(text); const detail = j?.errors?.[0]?.detail || ""; const m = detail.match(/available in (\d+)\s*seconds?/i); if (m) waitMs = (parseInt(m[1], 10) + 1) * 1000; } catch (e) {} Utilities.sleep(waitMs); sleepMs = Math.min(sleepMs * 2, 15000); continue; } throw new Error(`Klaviyo API error ${code}: ${text}`); } throw new Error("Klaviyo API error: exceeded retry attempts."); } function safeJson_(text) { try { return JSON.parse(text); } catch (e) { return null; } }