Our solution uses two functions for the monthly newsletter:
monthlyContent: An onRequest HTTP function that dynamically generates the newsletter HTML from Firestore posts.
Note: This guide focuses on the backend automation and Zoho API interaction, not the email's HTML generation. For the simplest implementation, you can host a static HTML page (ensuring all CSS is inline, as is standard for email) on any public server or CDN and provide that URL here.
Crucial: The URL must return raw HTML; it cannot be a client-side rendered page (like a React or Angular application) because Zoho’s servers do not execute JavaScript to generate the content.
sendMonthlyNewsletter: A scheduled function (onSchedule) that triggers on the first of the month, retrieves the content URL of the monthlyContent function, and sends the campaign via Zoho.
The Scheduled Send Function (sendMonthlyNewsletter)
This function executes the Zoho Campaign's two-step process: Create followed by Send.
Crucially, it uses the public URL of the monthlyContent function as the content_url parameter in the Zoho API call. This allows Zoho's servers to fetch the up-to-date HTML when the campaign is created.
We also log the campaign ID to Firestore for tracking and audit.
// Scheduled function to create and send monthly newsletter via Zoho Campaigns
exports.sendMonthlyNewsletter = onSchedule("0 5 1 * *", async (event) => {
const db = admin.firestore();
// 1. Initial Checks (e.g., if campaign is paused/weekly)
// ... (omitted for brevity, but check schedule setting here)
// The URL Zoho will fetch to get the email content
const contentUrl = `https://asia-southeast1-${process.env.GCLOUD_PROJECT}.cloudfunctions.net/monthlyContent`;
// Define metadata
const monthStr = new Date().toLocaleString("default", { month: "long", year: "numeric" });
const campaignName = `Monthly Newsletter - ${monthStr}`;
const subject = `Analyte Updates - ${monthStr}`;
const logDocRef = db.collection("newsletter_campaigns").doc(monthStr.replace(" ", "-"));
try {
const accessToken = await getZohoAccessToken("campaigns");
// --- A. CREATE CAMPAIGN ---
const createParams = new URLSearchParams({
resfmt: "json",
campaignname: campaignName,
subject,
content_url: contentUrl, // Zoho fetches HTML from this URL
list_details: JSON.stringify({ [process.env.ZOHO_LIST_KEY]: [] }),
// ... other params like from_email, topicId ...
});
const createCampaignUrl = `${process.env.ZOHO_CAMPAIGN_DOMAIN}/
api/v1.1/createCampaign`;
const createRes = await axios.post(
createCampaignUrl,
createParams.toString(),
{
headers: {
Authorization: `Zoho-oauthtoken ${accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
const campaignId = createRes.data.campaignKey;
await logDocRef.set({ campaignId, /* ... status logging ... */ }, { merge: true });
if (!campaignId) {
throw new Error("Campaign creation failed: " + createRes.data.message);
}
// --- B. SEND CAMPAIGN ---
const sendCampaignUrl = `${process.env.ZOHO_CAMPAIGN_DOMAIN}
/api/v1.1/sendcampaign`;
const sendParams = new URLSearchParams({
resfmt: "json",
campaignkey: campaignId,
});
const sendRes = await axios.post(sendCampaignUrl, sendParams.toString(), {
headers: {
Authorization: `Zoho-oauthtoken ${accessToken}`,
"Content-Type": "application/x-www-form-urlencoded",
},
});
const sendCode = sendRes.data.response?.code;
// 4. Log final status and recipient count
await logDocRef.update({
sendTime: new Date(),
sendSuccess: sendCode === 0,
/* ... more status logging ... */
});
return { success: sendCode === 0, campaignId };
} catch (error) {
// ... Log fatal error details ...
}
});
This architecture provides a scalable, cost-effective, and robust way to manage your newsletter workflow entirely within the Google Cloud ecosystem, integrated seamlessly with the power of Zoho Campaigns.