What it does
Iterates over all enabled ads in enabled ad groups and campaigns, fetches each final URL (and mobile final URL if configured), and checks the HTTP response code. Any URL returning a 404 is logged with its campaign, ad group, ad ID and URL, labelled with a configurable label (LP_404), and collected into an HTML email report that's sent to your configured recipients.
The code
/**
* Google Ads Script: 404 Checker with Email Alerts
* Scans all ENABLED ads in ENABLED ad groups and campaigns.
* Flags 404 on final URLs, labels them, logs them, and sends an email summary.
*/
var CONFIG = {
includeMobileFinalUrl: true,
labelName404: "LP_404",
userAgent: "Mozilla/5.0 (Google Ads 404 Checker)",
fetchTimeoutMs: 10000,
throttleMsBetweenFetches: 75,
maxAdsToProcess: 5000,
// Set to a URL to test one-off, or leave empty to skip the debug fetch.
DEBUG_TEST_URL: "", // e.g. "https://example.com/definitely-not-found-404"
// --- EMAIL SETTINGS ---
emailRecipients: [
"you@example.com" // <-- replace with your notification email(s)
],
emailSubject: "Google Ads 404 Alert - Broken Landing Pages Detected",
emailSenderName: "Google Ads 404 Checker"
};
function main() {
var start = new Date();
Logger.log("=== 404 CHECKER START === " + start.toISOString());
ensureLabel(CONFIG.labelName404);
var processed = 0;
var hits404 = 0;
var hitDetails = [];
try {
// Debug test URL
if (CONFIG.DEBUG_TEST_URL) {
var testStatus = getStatus(CONFIG.DEBUG_TEST_URL);
Logger.log("[DEBUG] Fetched " + CONFIG.DEBUG_TEST_URL + " -> " + testStatus);
}
// Select all enabled ads in enabled ad groups & campaigns
var adSelector = AdsApp.ads()
.withCondition("CampaignStatus = ENABLED")
.withCondition("AdGroupStatus = ENABLED")
.withCondition("Status = ENABLED")
.forDateRange("ALL_TIME")
.orderBy("CampaignName ASC")
.orderBy("AdGroupName ASC")
.orderBy("Id ASC")
.withLimit(CONFIG.maxAdsToProcess);
var it = adSelector.get();
while (it.hasNext()) {
var ad = it.next();
var finalUrl = safeGetFinalUrl(ad);
var mobileUrl = CONFIG.includeMobileFinalUrl ? safeGetMobileUrl(ad) : null;
// FINAL URL
if (finalUrl) {
var status = getStatus(finalUrl);
if (status === 404) {
hits404++;
hitDetails.push(recordHit(ad, "FINAL", finalUrl, status));
label404(ad);
}
politeSleep();
}
// MOBILE URL (if present and different)
if (mobileUrl && mobileUrl !== finalUrl) {
var mStatus = getStatus(mobileUrl);
if (mStatus === 404) {
hits404++;
hitDetails.push(recordHit(ad, "MOBILE", mobileUrl, mStatus));
label404(ad);
}
politeSleep();
}
processed++;
}
} catch (e) {
Logger.log("!! ERROR: " + e + "\n" + (e && e.stack ? e.stack : ""));
} finally {
logSummary(processed, hits404, start);
// Send email if 404s were found
if (hits404 > 0) {
sendEmailReport(hitDetails, processed, hits404, start);
}
}
}
/************** Helper Functions **************/
function ensureLabel(name) {
var it = AdsApp.labels().withCondition('Name = "' + name + '"').get();
if (!it.hasNext()) AdsApp.createLabel(name);
}
function safeGetFinalUrl(ad) {
try { return ad.urls().getFinalUrl(); } catch (e) { return null; }
}
function safeGetMobileUrl(ad) {
try { return ad.urls().getMobileFinalUrl(); } catch (e) { return null; }
}
function getStatus(url) {
try {
var resp = UrlFetchApp.fetch(url, {
method: "get",
followRedirects: true,
muteHttpExceptions: true,
validateHttpsCertificates: true,
headers: { "User-Agent": CONFIG.userAgent },
timeout: CONFIG.fetchTimeoutMs
});
return resp.getResponseCode();
} catch (e) {
Logger.log("Fetch error for " + url + " -> " + e);
return null;
}
}
function label404(ad) {
try { ad.applyLabel(CONFIG.labelName404); } catch (e) {}
}
function recordHit(ad, type, url, status) {
var campaign = ad.getCampaign().getName();
var adGroup = ad.getAdGroup().getName();
var adId = ad.getId();
var adType = "";
try { adType = ad.getType(); } catch (e) {}
var msg = "[404] " + type + " | Campaign: " + campaign +
" | AdGroup: " + adGroup +
" | AdID: " + adId +
" | Type: " + adType +
" | URL: " + url +
" | Status: " + status;
Logger.log(msg);
return { campaign: campaign, adGroup: adGroup, adId: adId, type: type, url: url, status: status };
}
function politeSleep() {
if (CONFIG.throttleMsBetweenFetches > 0) Utilities.sleep(CONFIG.throttleMsBetweenFetches);
}
function logSummary(processed, hits, start) {
var end = new Date();
Logger.log("=== 404 CHECKER END === " + end.toISOString());
Logger.log("Runtime (s): " + Math.round((end - start) / 1000));
Logger.log("Ads processed: " + processed);
Logger.log("404 hits: " + hits);
Logger.log("Label: " + CONFIG.labelName404);
}
/************** EMAIL REPORT **************/
function sendEmailReport(hitDetails, processed, hits404, start) {
var end = new Date();
var runtime = Math.round((end - start) / 1000);
var html = `
<h2>Google Ads 404 Checker Report</h2>
<p><strong>Date:</strong> ${start.toISOString()}</p>
<p><strong>Runtime:</strong> ${runtime} seconds</p>
<p><strong>Total Ads Processed:</strong> ${processed}</p>
<p><strong>404 Errors Found:</strong> ${hits404}</p>
<hr>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;">
<tr><th>Campaign</th><th>Ad Group</th><th>Ad ID</th><th>Type</th><th>Status</th><th>URL</th></tr>
${hitDetails.map(function(h) {
return `<tr>
<td>${h.campaign}</td>
<td>${h.adGroup}</td>
<td>${h.adId}</td>
<td>${h.type}</td>
<td>${h.status}</td>
<td><a href="${h.url}" target="_blank">${h.url}</a></td>
</tr>`;
}).join("")}
</table>
<p>All affected ads have been labeled <strong>${CONFIG.labelName404}</strong>.</p>
`;
MailApp.sendEmail({
to: CONFIG.emailRecipients.join(","),
subject: CONFIG.emailSubject,
htmlBody: html,
name: CONFIG.emailSenderName
});
Logger.log("Email sent to: " + CONFIG.emailRecipients.join(", "));
}
When to use this
Run this when landing pages change or are retired, after a site migration or redesign, when you suspect broken final URLs are hurting Quality Score and conversion rate, or as a scheduled weekly health check so broken landing pages are caught before they waste spend.
Prerequisites
- Script access at the account or MCC level (Tools & Settings > Bulk Actions > Scripts).
- Ability to authorize MailApp for the notification email.
- Set CONFIG.emailRecipients to your email(s).
How to implement
- 1
Create a new script
Tools & Settings > Bulk Actions > Scripts > + New Script. Paste the full file.
- 2
Configure the CONFIG block
Set CONFIG.emailRecipients to your notification email(s). Optionally set a DEBUG_TEST_URL to verify it catches a 404 before running for real.
- 3
Run once to test
Run the script manually and check the Logs (View > Logs) for the summary and any 404 hits. Confirm the email arrives.
- 4
Schedule it
Set a daily or weekly schedule (clock icon on the script). Weekly is usually enough unless landing pages change often.
Caveats
Checks the final URL response code, so a page that redirects to a 404 will be caught, but a soft-404 page (returns 200 with 'not found' content) will not be flagged. Fetching thousands of URLs on a large account can take a while and count against Google's execution quota, so maxAdsToProcess is capped at 5000 by default. Mobile final URLs are only checked when they differ from the desktop final URL.