การส่งออกรายชื่อผู้ลงทะเบียนงานสิบสองคนครั้งเดียวทำได้ง่าย แต่ครั้งที่สองอาจมีอีเมลซ้ำและมีคนยกเลิกรับข่าวสารใน Brevo การนำเข้าที่ทำซ้ำได้ต้องตรวจทั้งสองอย่าง
สคริปต์ใช้ endpoint ของ Brevo สำหรับอ่าน สร้าง และอัปเดตผู้ติดต่อ เหมาะกับการนำเข้าขนาดเล็กตามเวลา ไม่ใช่เครื่องมือโหลดจำนวนมากหรือคอนเนกเตอร์ Google Sheets ในตัว ไม่ส่งอีเมล คุณต้องดูแลหลักฐานความยินยอมและการทำงานของสคริปต์
เตรียม Sheet และรายการ
สร้างแท็บ Contacts ด้วยหัวคอลัมน์ต่อไปนี้ให้ตรง ใช้ช่องทำเครื่องหมายใน consent และให้ TRUE เมื่อได้รับอนุญาตสำหรับการตลาดที่ตั้งใจเท่านั้น
| firstName | lastName | consent | |
|---|---|---|---|
| [email protected] | Jane | Doe | TRUE |
| [email protected] | Alex | Lee | FALSE |
สร้างหรือเลือกรายการ Brevo แล้วใส่ ID ตัวเลขใน LIST_ID ตรวจว่ามีแอตทริบิวต์ FNAME และ LNAME ใน Brevo ตัวอย่างจับคู่แค่สองฟิลด์นี้ หาก Sheet ไม่พิสูจน์เวลาและวิธีที่ได้รับความยินยอม ให้เก็บบันทึกต้นทางแยกไว้ เซลล์ TRUE อย่างเดียวไม่ใช่หลักฐาน
ใน Extensions → Apps Script → Project Settings → Script Properties เพิ่ม BREVO_API_KEY คุณสมบัตินี้ใช้ร่วมกันในผู้ใช้สคริปต์ จึงควรจำกัดสิทธิ์แก้ไข อย่าใส่คีย์ในเซลล์หรือโค้ด อ่าน กฎการเข้าถึงของ Google
สคริปต์
แทนที่ Code.gs ด้วยตัวอย่างนี้ และตั้ง LIST_ID ก่อนเรียกครั้งแรก
const LIST_ID = 42; // Replace with your existing Brevo list ID.const TAB = 'Contacts';const BASE = 'https://api.brevo.com/v3';
function syncSheetToBrevo() { const key = PropertiesService.getScriptProperties().getProperty('BREVO_API_KEY'); if (!key) throw new Error('Set BREVO_API_KEY in Script Properties first'); const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TAB); if (!sheet) throw new Error('Contacts tab missing'); const values = sheet.getDataRange().getValues(); const headers = values.shift().map(h => String(h).trim()); const columns = ['email', 'firstName', 'lastName', 'consent'] .map(name => headers.indexOf(name)); if (columns.includes(-1)) throw new Error('Required header missing'); const [emailCol, firstCol, lastCol, consentCol] = columns; const seen = new Set(); const result = { written: 0, skipped: 0 };
for (const row of values) { const email = String(row[emailCol] || '').trim().toLowerCase(); if (row[consentCol] !== true || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || seen.has(email)) { result.skipped++; continue; } seen.add(email); const path = '/contacts/' + encodeURIComponent(email); const current = brevo_(key, path, 'get'); if (current.code !== 200 && current.code !== 404) { throw new Error(`Contact read failed (${current.code}) for row ${result.written + result.skipped + 2}`); } if (current.code === 200) { const contact = JSON.parse(current.text); if (contact.emailBlacklisted || (contact.listUnsubscribed || []).includes(LIST_ID)) { result.skipped++; continue; } } const attributes = {}; if (row[firstCol] !== '') attributes.FNAME = String(row[firstCol]); if (row[lastCol] !== '') attributes.LNAME = String(row[lastCol]); const body = { attributes, listIds: [LIST_ID] }; const saved = current.code === 404 ? brevo_(key, '/contacts', 'post', { email, ...body }) : brevo_(key, path, 'put', body); if (![201, 204].includes(saved.code)) { throw new Error(`Contact write failed (${saved.code}) for ${email}: ${saved.text.slice(0, 200)}`); } result.written++; } Logger.log(JSON.stringify(result)); return result;}
function brevo_(key, path, method, body) { for (let attempt = 0; attempt < 3; attempt++) { const response = UrlFetchApp.fetch(BASE + path, { method, contentType: 'application/json', headers: { 'api-key': key }, payload: body ? JSON.stringify(body) : undefined, muteHttpExceptions: true, }); const code = response.getResponseCode(); if (code !== 429) return { code, text: response.getContentText() }; if (attempt === 2) return { code, text: response.getContentText() }; const reset = Object.entries(response.getHeaders()).find( ([name]) => name.toLowerCase() === 'x-sib-ratelimit-reset' ); const raw = Number(reset && reset[1]); const seconds = Number.isFinite(raw) && raw > 0 ? raw : 2 ** attempt; const waitSeconds = seconds + 2 ** attempt; if (waitSeconds > 60) return { code, text: response.getContentText() }; Utilities.sleep(waitSeconds * 1000); }}บันทึก เลือก syncSheetToBrevo แล้วกด Run Google จะขอสิทธิ์เข้าถึง Sheets และคำขอภายนอก ดู log {written, skipped} ใน Executions หากอ่านได้ 404 จะสร้างผู้ติดต่อ ส่วน 200 จะอัปเดต ข้อผิดพลาดอื่นหยุดงาน การเขียนผู้ติดต่อสำเร็จไม่ใช่การส่งแคมเปญ
สคริปต์ลบอีเมลซ้ำในครั้งเดียว และตรวจ Brevo ใหม่ในการเรียกครั้งถัดไป ไม่ตั้ง emailBlacklisted เป็น false และไม่เปลี่ยนที่อยู่ หาก Brevo แจ้งว่าบล็อกทั่วไปหรือยกเลิกจากรายการเป้าหมาย แถวจะถูกข้ามแม้ Sheet ยังเป็น TRUE อย่าใช้เพื่อยกเลิกการเลิกรับ Brevo เตือน ว่าเปลี่ยนอีเมลของผู้ถูกบล็อกอาจปลดบล็อก
ตรวจหนึ่งครั้งก่อนตั้งเวลา
ใช้รายการทดสอบและผู้ติดต่อที่คุณควบคุม เรียกด้วยแถว TRUE หนึ่งแถว FALSE หนึ่งแถว และอีเมลซ้ำ ควรเขียนหนึ่งและข้ามสอง ยกเลิกรับผู้ติดต่อทดสอบใน Brevo แล้วเรียกอีกครั้ง ต้องไม่มีการเขียนสำหรับคนนั้น หากต้องการทดสอบข้อผิดพลาด ให้ตั้ง LIST_ID ผิดชั่วคราว ดู HTTP error ใน Executions แล้วคืนค่าและลองใหม่ อย่าทดสอบกับข้อมูลลูกค้าจริง โค้ดนี้ผ่านการทดสอบในเครื่องด้วยการตอบกลับ Brevo จำลอง ไม่มีการเขียนจริง
เปิด Apps Script → Triggers → Add Trigger เลือก syncSheetToBrevo และช่วงเวลา ทริกเกอร์ทำงานด้วยบัญชีของผู้สร้าง ซึ่งต้องยังเข้าถึง Sheet ได้ เริ่มวันละครั้งสำหรับ Sheet เล็กและดูข้อผิดพลาด Google จำกัดเวลารันและการใช้ URL Fetch แบ่งแหล่งใหญ่เป็นงานที่มีขอบเขต
เมื่อได้ 429 สคริปต์อ่าน x-sib-ratelimit-reset และลองใหม่ไม่เกินสองครั้ง ถ้าต้องรอเกิน 60 วินาที จะส่งคืน 429 ให้การเรียกครั้งหน้าแทนการลองก่อนเวลา ข้อผิดพลาดอื่นก็หยุด และครั้งถัดไปเริ่มจากต้น ผู้ติดต่ออาจยกเลิกรับระหว่างอ่านกับเขียน ใช้ตัวอย่างเมื่อยอมรับช่วงเวลาสั้นนี้ได้ หากต้องเข้มงวด ต้องกระทบยอดเหตุการณ์ มีแหล่งความยินยอมที่เชื่อถือได้ และตรวจครั้งสุดท้ายก่อนส่ง
Sheet ขนาดใหญ่ใช้ การนำเข้าแบบอะซิงโครนัสของ Brevo ซึ่งคืน processId นั่นหมายถึงรับงาน ไม่ใช่เสร็จงาน ตรวจผลก่อนแจ้งสำเร็จ วิธีเขียนทีละผู้ติดต่อในบทความนี้ช่วยระบุแถวที่ผิดพลาด
คำถามที่พบบ่อย
ต้องมีคอนเนกเตอร์ Google Sheets ในตัวหรือไม่?
ไม่ต้อง ตัวอย่างทำงานในโครงการ Apps Script ที่ผูกกับ Sheet และเรียก API ผู้ติดต่อ Brevo คุณดูแลสคริปต์ คีย์ API และทริกเกอร์ตามเวลาเอง
แถวใดถูกส่งไป Brevo?
เฉพาะแถวที่ consent เป็น TRUE และอีเมลถูกต้อง สคริปต์ลบรายการซ้ำในแต่ละครั้งและตรวจ Brevo ก่อนเขียน
เรียกซ้ำจะสมัครคนที่ยกเลิกรับแล้วอีกหรือไม่?
สคริปต์ข้ามผู้ติดต่อที่ Brevo ระบุ emailBlacklisted หรือยกเลิกจากรายการเป้าหมาย ไม่ส่ง emailBlacklisted: false และไม่เปลี่ยนอีเมล ก่อนใช้งานจริงควรตรวจแหล่งความยินยอมและการบล็อกอื่นด้วย
จะลองงานที่ล้มเหลวใหม่อย่างไร?
อ่านข้อผิดพลาดใน Apps Script Executions แก้สาเหตุ แล้วเรียกฟังก์ชันเดิมอีกครั้ง การใช้ที่อยู่และ ID รายการเดิมทำให้แถวที่สำเร็จไม่สร้างผู้ติดต่อคนที่สอง