🔑
Google Account
Access to Google Search Console with at least Restricted User permissions on at least one verified property.
☁️
Google Cloud Project
A Google Cloud project (free tier is sufficient). You'll create one in Section 5 if you don't have one.
📋
JavaScript Basics
Apps Script uses JavaScript. Familiarity with variables, functions, and JSON is helpful but all code is fully annotated.

1. What Is the Google Search Console API?

The Google Search Console API is a REST API that provides programmatic, authenticated access to the search performance data available in the Google Search Console interface. It enables SEO teams to retrieve clicks, impressions, click-through rate (CTR), and average position data at the query, page, country, device, and date dimension — without manual CSV exports.

The API is formally called the Google Search Console API v3 (webmasters API v3) and exposes several distinct endpoint categories. The most commonly used is the Search Analytics API, which returns the same underlying data as the Performance report in GSC but with programmatic control over dimensions, filters, date ranges, and row counts.

Why This Matters for SEO Teams

The GSC interface limits data export to 1,000 rows and requires manual date range selection. The API removes both constraints — returning up to 25,000 rows per request, with pagination to access millions of rows, and supporting any date range within the 16-month data window. This unlocks query-level analysis at scale that is impossible through the web interface alone.

The Google Apps Script layer is what makes this accessible without a dedicated server. Apps Script is a cloud-based JavaScript runtime built into Google Workspace — it runs on Google's infrastructure, is triggered on schedules, and outputs directly to Google Sheets. The combination of the Search Console API and Apps Script is effectively a free, serverless SEO data pipeline.

2. What Can You Build?

The following use cases represent the most common and highest-value implementations of the Search Console API and Apps Script combination. Each is covered in this guide with working code.

Use Case 01
Automated Weekly Report
Pull the last 7 days of top queries, pages, and performance metrics into a Google Sheet automatically every Monday morning. No manual export, no copy-paste.
Use Case 02
CTR Drop Alert
Compare CTR for top queries week-over-week and send a Gmail alert when any high-impression query drops more than a defined CTR threshold.
Use Case 03
Position Tracking Sheet
Build a running historical position tracker for a defined keyword set — updated daily, stored row-by-row in Sheets, charted automatically.
Use Case 04
Page-Level Performance Audit
Pull clicks and impressions per URL for the last 90 days, identify pages with high impressions but low clicks (strong title/meta candidates), and flag them in a review tab.
Use Case 05
Device & Country Breakdown
Dimension performance by device type (mobile/desktop/tablet) and country to identify mobile CTR gaps or market-specific ranking opportunities.
Use Case 06
Multi-Property Dashboard
Aggregate GSC data across multiple site properties into a single Sheets dashboard — critical for agencies managing multiple client domains from one automation.

3. Search Console API Endpoints Reference

The Search Console API exposes five endpoint categories. This guide focuses primarily on Search Analytics, which is the most data-rich and most commonly automated endpoint. The others are documented here for reference.

POST

Search Analytics — Query

POST https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query

Returns clicks, impressions, CTR, and average position. Accepts dimensions (query, page, country, device, date, searchAppearance), date ranges, filters, and aggregation type. The primary endpoint for all performance data.

GET

Sites — List

GET https://searchconsole.googleapis.com/webmasters/v3/sites

Returns all verified site properties the authenticated user has access to. Useful for multi-property scripts to enumerate domains dynamically rather than hard-coding property URLs.

GET

URL Inspection

POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect

Returns index status, crawl status, canonical URL, and mobile usability verdict for a specific URL. Useful for bulk index coverage checks across a URL list. Requires a separate scope: https://www.googleapis.com/auth/webmasters

GET

Sitemaps — List

GET https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/sitemaps

Returns all sitemaps submitted for a property with submission date, last download date, and warning/error counts. Useful for automated sitemap health monitoring.

GET

Index Coverage — Issues

GET https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/indexStatusResults

Returns index coverage issue categories (Crawled – currently not indexed, Discovered – currently not indexed, etc.) with issue counts. Enables automated index health reporting and issue spike alerts.

Search Analytics Dimensions Reference

DimensionDescriptionExample ValueNotes
queryThe search query string"search console api"Some queries anonymized as "(not provided)"
pageThe URL that appeared in resultshttps://yourdomain.com/page/Full canonical URL including protocol
countryISO 3166-1 alpha-3 country codeUSA, GBR, DEUBased on user location
deviceDevice categoryMOBILE, DESKTOP, TABLETValues are uppercase strings
dateCalendar date2026-07-01YYYY-MM-DD format
searchAppearanceSearch feature typeRICH_SNIPPET, AMP, VIDEOCannot combine with other dimensions

4. API Quotas and Rate Limits

Understanding the Search Console API quota system prevents failed scripts and unexpected errors in production. All quotas are per Google Cloud project unless otherwise noted.

Requests per day (per project)
2,000 requests / day
Requests per minute (per user)
1,200 req / min
Max rows per Search Analytics request
25,000 rows
Historical data window
~16 months
URL Inspection requests per day
2,000 / day
Quota Strategy for Large Sites

A site with 100,000+ unique query-page combinations will exceed the 25,000-row limit per request. Use the startRow parameter to paginate through results in 25,000-row batches. Each batch consumes one API request. A full pull of 200,000 rows requires 8 requests — well within the 2,000/day daily quota but important to account for when running multiple scripts or multiple properties under one project.

5. Google Cloud Console Setup

The Search Console API requires a Google Cloud project with the API enabled and OAuth credentials configured. This section walks through setup from scratch — estimated time: 10 minutes.

  1. Create or select a Google Cloud project

    Navigate to console.cloud.google.com. In the top navigation bar, click the project selector dropdown and choose New Project. Name it something recognizable (e.g., gsc-appscript-automation). Click Create. Wait for project creation to complete — this takes 10–30 seconds.

  2. Enable the Search Console API

    In the left sidebar, navigate to APIs & Services → Library. Search for Google Search Console API. Click the result (published by Google), then click Enable. The API status will change to "Enabled" — this is required before any API calls will succeed.

  3. Configure the OAuth consent screen

    Navigate to APIs & Services → OAuth consent screen. Select Internal if your Google account is a Google Workspace account (recommended for agency use). Select External if using a personal Gmail account. Fill in the App name, user support email, and developer contact email. Click Save and Continue through the Scopes and Test Users screens.

  4. Add the required OAuth scope

    On the Scopes screen, click Add or Remove Scopes. In the filter field, enter webmasters. Check the scope https://www.googleapis.com/auth/webmasters.readonly for read-only data access. If you need URL inspection write access (to submit URLs for indexing), also add https://www.googleapis.com/auth/webmasters. Click Update then Save and Continue.

6. OAuth 2.0 Credentials Configuration

OAuth 2.0 is the authentication standard the Search Console API requires. You'll create a client ID and client secret that your Apps Script uses to obtain access tokens on behalf of your Google account.

  1. Create OAuth 2.0 credentials

    In Google Cloud Console, navigate to APIs & Services → Credentials. Click + Create Credentials → OAuth Client ID. Set Application type to Web application. Give it a name (e.g., GSC Apps Script Client).

  2. Add the Apps Script redirect URI

    Under Authorized redirect URIs, click Add URI and paste the following URI exactly:

    Redirect URIURI
    https://script.google.com/macros/d/{SCRIPT_ID}/usercallback
    
    Note: You'll replace {SCRIPT_ID} with your actual Apps Script
    project ID after creating it in the next section.
    For now, add a placeholder and return to update it later.
  3. Copy your Client ID and Client Secret

    After clicking Create, a modal displays your Client ID and Client Secret. Copy both — you'll paste them into Apps Script in the next section. You can always retrieve them again from Credentials → your OAuth client → Edit.

Security Note

Never hardcode your Client ID or Client Secret directly in Apps Script code that is shared, published, or committed to version control. Store credentials in Script Properties (Project Settings → Script Properties) and reference them via PropertiesService.getScriptProperties().getProperty('CLIENT_ID'). Script Properties are private to the script owner and are not visible in the code editor.

7. Apps Script Project Setup

  1. Create a new Apps Script project

    Navigate to script.google.com and click New Project. Rename the project from "Untitled project" to something descriptive — e.g., GSC Search Analytics Automation. The editor will open with a default Code.gs file.

  2. Note your Script ID and update the redirect URI

    In the Apps Script editor, click Project Settings (the gear icon). Your Script ID is shown at the top. Copy it, then return to Google Cloud Console → Credentials → your OAuth client → Edit, and replace the placeholder in the redirect URI with your actual Script ID:

    Updated Redirect URIURI
    https://script.google.com/macros/d/1BxiMV...AbCdEfGhIjKlMnOpQrStUv/usercallback
  3. Link your Google Cloud project

    In Apps Script Project Settings, under Google Cloud Platform (GCP) Project, click Change project. Enter the Project Number of the Cloud project you created earlier (found in the Cloud Console home page → Project info card). Click Set project. This links your Apps Script to the Cloud project with the Search Console API enabled.

  4. Store credentials in Script Properties

    In Apps Script Project Settings, scroll to Script Properties. Add two properties:

    Script PropertiesKEY/VALUE
    CLIENT_ID       your-client-id.apps.googleusercontent.com
    CLIENT_SECRET   your-client-secret-value
  5. Install the OAuth2 library

    In the Apps Script editor, click the + icon next to Libraries. Paste the following Script ID into the search field and click Look up:

    OAuth2 Library Script IDID
    1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF

    Select the latest version, set the identifier to OAuth2, and click Add. This library handles the OAuth 2.0 token flow, including refresh token management.

8. The Authentication Function

With the OAuth2 library installed and credentials stored in Script Properties, the following three functions establish and manage authentication. Paste these into your Code.gs file.

Authentication — Code.gs
Apps Script / JS
/**
 * Returns an authenticated OAuth2 service for the Search Console API.
 * Call this function from any other function that needs API access.
 */
function getSearchConsoleService() {
  const props = PropertiesService.getScriptProperties();

  return OAuth2.createService('SearchConsole')
    // Set the endpoint URLs
    .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
    .setTokenUrl('https://accounts.google.com/o/oauth2/token')

    // Set the OAuth credentials from Script Properties
    .setClientId(props.getProperty('CLIENT_ID'))
    .setClientSecret(props.getProperty('CLIENT_SECRET'))

    // Use the script's properties store for token persistence
    .setPropertyStore(props)

    // Set the callback function name (must match the function below)
    .setCallbackFunction('authCallback')

    // Request read-only access to Search Console data
    .setScope('https://www.googleapis.com/auth/webmasters.readonly')

    // Required for Google APIs to return a refresh token
    .setParam('access_type', 'offline')
    .setParam('prompt', 'consent');
}

/**
 * Handles the OAuth callback. Apps Script calls this automatically
 * after the user authorizes access in the browser prompt.
 */
function authCallback(request) {
  const service = getSearchConsoleService();
  const authorized = service.handleCallback(request);
  return HtmlService.createHtmlOutput(
    authorized
      ? '✅ Authorized. You can close this tab and return to Apps Script.'
      : '❌ Authorization denied. Close this tab and try again.'
  );
}

/**
 * Run this function once to trigger the OAuth authorization flow.
 * Check the Apps Script execution log for the authorization URL,
 * then open it in your browser to grant access.
 */
function authorizeScript() {
  const service = getSearchConsoleService();
  if (!service.hasAccess()) {
    const authUrl = service.getAuthorizationUrl();
    Logger.log('Open this URL to authorize:\n%s', authUrl);
  } else {
    Logger.log('✅ Already authorized.');
  }
}

After pasting this code, run the authorizeScript function once by selecting it in the function dropdown and clicking Run. Check the Execution Log — it will print an authorization URL. Open that URL in your browser, sign in with the Google account that has GSC access, and click Allow. The OAuth callback will store your access token in Script Properties automatically. You only need to do this once.

Verification

After authorizing, run authorizeScript again. The log should now print ✅ Already authorized. — confirming the access token was stored successfully. If you see the authorization URL again, the token was not stored: verify that your Cloud project is correctly linked and your Client ID and Secret are in Script Properties.

9. Your First Search Analytics API Call

With authentication in place, the following function makes your first call to the Search Analytics API — pulling the top 10 queries by clicks for the last 28 days on your property.

First API Call — top queries, last 28 days
Apps Script / JS
/**
 * Fetches the top 10 queries by clicks for the last 28 days.
 * Replace SITE_URL with your verified property URL, e.g.:
 *   'https://www.yourdomain.com/'  (for URL-prefix properties)
 *   'sc-domain:yourdomain.com'     (for domain properties)
 */
function fetchTopQueries() {
  const SITE_URL  = 'https://www.yourdomain.com/';
  const service   = getSearchConsoleService();

  // Guard: abort if not authorized
  if (!service.hasAccess()) {
    Logger.log('Not authorized. Run authorizeScript() first.');
    return;
  }

  // Calculate date range: last 28 days (GSC data lags ~2 days)
  const today     = new Date();
  const endDate   = formatDate(new Date(today - 2 * 86400000));
  const startDate = formatDate(new Date(today - 29 * 86400000));

  // Build the API request body
  const payload = {
    startDate:  startDate,
    endDate:    endDate,
    dimensions: ['query'],          // group results by search query
    rowLimit:   10,                // top 10 only for this demo
    startRow:   0                  // pagination offset (0 = first page)
  };

  // Construct the API endpoint URL
  const endpoint =
    `https://searchconsole.googleapis.com/webmasters/v3/sites/` +
    encodeURIComponent(SITE_URL) +
    '/searchAnalytics/query';

  // Make the authenticated POST request
  const response = UrlFetchApp.fetch(endpoint, {
    method:  'POST',
    headers: {
      'Authorization': 'Bearer ' + service.getAccessToken(),
      'Content-Type':  'application/json'
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true     // prevents script crash on API errors
  });

  // Parse and log the response
  const data = JSON.parse(response.getContentText());

  if (data.error) {
    Logger.log('API Error: %s', JSON.stringify(data.error));
    return;
  }

  // Log each row: query | clicks | impressions | ctr | position
  (data.rows || []).forEach(row => {
    Logger.log(
      '%-40s | clicks: %-5s | impr: %-6s | ctr: %s% | pos: %s',
      row.keys[0],                               // query string
      row.clicks,
      row.impressions,
      (row.ctr * 100).toFixed(1),
      row.position.toFixed(1)
    );
  });
}

// Utility: format a Date object as YYYY-MM-DD
function formatDate(date) {
  return Utilities.formatDate(date, 'GMT', 'yyyy-MM-dd');
}

Run fetchTopQueries from the Apps Script editor. The Execution Log will display your top 10 queries with clicks, impressions, CTR, and average position. If you see an API error, check Section 15 (Troubleshooting) for the most common causes.

API Response Structure

Example API ResponseJSON
{
  "rows": [
    {
      "keys": ["search console api apps script"],
      "clicks":      142,
      "impressions": 1840,
      "ctr":         0.0771,   // multiply by 100 for percentage
      "position":    3.2       // average position (1.0 = top result)
    },
    {
      "keys": ["gsc api tutorial"],
      "clicks":      98,
      "impressions": 2100,
      "ctr":         0.0467,
      "position":    5.8
    }
    // ... more rows
  ],
  "responseAggregationType": "byPage"
}

10. Writing GSC Data to Google Sheets

The following script extends the basic query to write data directly into a named Google Sheet tab — creating headers if they don't exist, clearing the previous run's data, and writing fresh rows each time it executes.

Export to Sheets — full query + page report
Apps Script / JS
/**
 * Pulls top 500 query+page combinations from GSC (last 28 days)
 * and writes them to a sheet named 'GSC_Report'.
 * Creates the sheet and headers if they don't exist.
 */
function exportGscToSheets() {
  const SITE_URL   = 'https://www.yourdomain.com/';
  const SHEET_NAME = 'GSC_Report';
  const ROW_LIMIT  = 500;

  const service = getSearchConsoleService();
  if (!service.hasAccess()) {
    Logger.log('Not authorized. Run authorizeScript() first.');
    return;
  }

  // ── Date range ──────────────────────────────────────────────────
  const today     = new Date();
  const endDate   = formatDate(new Date(today - 2  * 86400000));
  const startDate = formatDate(new Date(today - 29 * 86400000));

  // ── API request ─────────────────────────────────────────────────
  const payload = {
    startDate,
    endDate,
    dimensions: ['query', 'page'],    // row per query+page combination
    rowLimit:   ROW_LIMIT
  };

  const endpoint =
    'https://searchconsole.googleapis.com/webmasters/v3/sites/' +
    encodeURIComponent(SITE_URL) +
    '/searchAnalytics/query';

  const response = UrlFetchApp.fetch(endpoint, {
    method:  'POST',
    headers: {
      'Authorization': 'Bearer ' + service.getAccessToken(),
      'Content-Type':  'application/json'
    },
    payload:           JSON.stringify(payload),
    muteHttpExceptions: true
  });

  const data = JSON.parse(response.getContentText());
  if (data.error) { Logger.log(data.error); return; }

  // ── Google Sheets setup ─────────────────────────────────────────
  const ss    = SpreadsheetApp.getActiveSpreadsheet();
  let   sheet = ss.getSheetByName(SHEET_NAME);

  // Create the sheet if it doesn't exist
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
  }

  // Clear previous data (preserves formatting)
  sheet.clearContents();

  // ── Write headers ───────────────────────────────────────────────
  const headers = [
    'Query', 'Page', 'Clicks', 'Impressions', 'CTR',
    'Avg Position', 'Date Range', 'Exported At'
  ];
  sheet.appendRow(headers);

  // Style the header row
  sheet.getRange(1, 1, 1, headers.length)
       .setFontWeight('bold')
       .setBackground('#1a1a1a')
       .setFontColor('#bf953f');

  // ── Write data rows ─────────────────────────────────────────────
  const exportedAt  = new Date().toISOString();
  const dateRangeStr = `${startDate} to ${endDate}`;

  const rows = (data.rows || []).map(row => [
    row.keys[0],                          // query
    row.keys[1],                          // page URL
    row.clicks,
    row.impressions,
    (row.ctr * 100).toFixed(2) + '%',   // format as percentage
    row.position.toFixed(1),
    dateRangeStr,
    exportedAt
  ]);

  if (rows.length > 0) {
    sheet.getRange(2, 1, rows.length, headers.length)
         .setValues(rows);
  }

  Logger.log('✅ Written %s rows to sheet "%s"', rows.length, SHEET_NAME);
}
Running the Script from a Sheet

This script must be opened from a Google Sheets bound script — meaning you open it via Extensions → Apps Script from within the Google Sheet you want it to write to. SpreadsheetApp.getActiveSpreadsheet() returns the Sheet the script is bound to. If you created the script at script.google.com directly (a standalone script), replace getActiveSpreadsheet() with SpreadsheetApp.openById('your-spreadsheet-id').

11. Pagination for Large Sites

Sites with substantial query volume will exceed the 25,000-row limit per API request. The following function handles pagination automatically — looping requests with an incrementing startRow offset until all rows are retrieved.

Paginated fetch — handles 25K+ row sites
Apps Script / JS
/**
 * Fetches ALL query data from GSC using paginated requests.
 * Each request returns up to 25,000 rows; loops until exhausted.
 * Returns a flat array of all rows across all pages.
 */
function fetchAllRows(siteUrl, startDate, endDate, dimensions) {
  const service    = getSearchConsoleService();
  const PAGE_SIZE  = 25000;
  const endpoint   =
    'https://searchconsole.googleapis.com/webmasters/v3/sites/' +
    encodeURIComponent(siteUrl) + '/searchAnalytics/query';

  let allRows  = [];
  let startRow = 0;
  let page     = 0;

  while (true) {
    page++;
    Logger.log('Fetching page %s (startRow: %s)...', page, startRow);

    const payload = {
      startDate, endDate, dimensions,
      rowLimit:  PAGE_SIZE,
      startRow
    };

    const response = UrlFetchApp.fetch(endpoint, {
      method:  'POST',
      headers: {
        'Authorization': 'Bearer ' + service.getAccessToken(),
        'Content-Type':  'application/json'
      },
      payload:           JSON.stringify(payload),
      muteHttpExceptions: true
    });

    const data = JSON.parse(response.getContentText());
    if (data.error) { Logger.log(data.error); break; }

    const pageRows = data.rows || [];
    allRows = allRows.concat(pageRows);

    Logger.log('  → %s rows returned. Total so far: %s', pageRows.length, allRows.length);

    // If fewer than PAGE_SIZE rows returned, we've reached the last page
    if (pageRows.length < PAGE_SIZE) break;

    startRow += PAGE_SIZE;

    // Polite delay to avoid hitting rate limits on large datasets
    Utilities.sleep(500);
  }

  Logger.log('✅ Total rows fetched: %s across %s pages', allRows.length, page);
  return allRows;
}

12. Scheduling Automated Reports with Triggers

Apps Script triggers run functions on a schedule without any manual execution. Setting up a weekly trigger means your GSC data is refreshed in Sheets automatically every week — a fully hands-off reporting pipeline.

  1. Open the Triggers panel

    In the Apps Script editor, click the clock icon in the left sidebar (or navigate to Triggers from the menu). Click + Add Trigger in the bottom-right.

  2. Configure the trigger settings

    In the trigger configuration panel:

    Trigger ConfigurationSETTINGS
    Choose function to run:  exportGscToSheets
    Choose deployment:       Head
    Select event source:    Time-driven
    Select type of trigger: Week timer
    Select day of week:     Every Monday
    Select time of day:     6am to 7am

    Click Save. Apps Script may prompt you to grant additional permissions the first time a trigger is saved — approve them with the same Google account that has GSC access.

Once saved, the trigger will appear in your Triggers list with its next execution time. You can create multiple triggers for the same or different functions — for example, a daily trigger for position tracking and a weekly trigger for the full query report.

Apps Script Execution Time Limit

Apps Script functions have a maximum execution time of 6 minutes for free Google accounts and 30 minutes for Google Workspace accounts. Scripts that paginate through very large datasets (200,000+ rows) may approach this limit. If you hit it, split the export into multiple functions — one per date range segment — and chain them with separate triggers or a continuation token pattern.

13. Advanced: CTR Drop Alert Script

This script compares CTR for your top queries week-over-week and sends an email alert when any high-impression query drops below a defined CTR threshold — enabling proactive response to SERP changes before they show up in traffic metrics.

CTR Drop Alert — weekly email notification
Apps Script / JS
/**
 * Compares top-query CTR between the current week and last week.
 * Sends a Gmail alert for any query where:
 *   - Impressions >= MIN_IMPRESSIONS (to filter low-traffic noise)
 *   - CTR dropped by >= CTR_DROP_THRESHOLD percentage points
 */
function ctrDropAlert() {
  const SITE_URL           = 'https://www.yourdomain.com/';
  const ALERT_EMAIL        = 'youremail@domain.com';
  const MIN_IMPRESSIONS    = 100;    // ignore low-impression queries
  const CTR_DROP_THRESHOLD = 2;       // alert if CTR drops 2+ percentage points

  const service = getSearchConsoleService();
  if (!service.hasAccess()) { Logger.log('Not authorized.'); return; }

  // Calculate date ranges for this week and last week (both lag 2 days)
  const today    = new Date();
  const d        = n => new Date(today - n * 86400000);
  const thisWeek = { start: formatDate(d(9)),  end: formatDate(d(2)) };
  const lastWeek = { start: formatDate(d(16)), end: formatDate(d(9)) };

  // Fetch helper: returns a Map of query → {clicks, impressions, ctr, position}
  const fetchQueryMap = (startDate, endDate) => {
    const endpoint =
      'https://searchconsole.googleapis.com/webmasters/v3/sites/' +
      encodeURIComponent(SITE_URL) + '/searchAnalytics/query';
    const resp = UrlFetchApp.fetch(endpoint, {
      method: 'POST',
      headers: { 'Authorization': 'Bearer ' + service.getAccessToken(),
                 'Content-Type':  'application/json' },
      payload: JSON.stringify({ startDate, endDate,
               dimensions: ['query'], rowLimit: 1000 }),
      muteHttpExceptions: true
    });
    const parsed = JSON.parse(resp.getContentText());
    const map = new Map();
    (parsed.rows || []).forEach(r => map.set(r.keys[0], r));
    return map;
  };

  const currentMap = fetchQueryMap(thisWeek.start, thisWeek.end);
  const previousMap = fetchQueryMap(lastWeek.start, lastWeek.end);

  // Find queries with significant CTR drops
  const drops = [];
  currentMap.forEach((curr, query) => {
    const prev = previousMap.get(query);
    if (!prev) return;  // skip queries with no prior week data

    const currCtr = curr.ctr * 100;
    const prevCtr = prev.ctr * 100;
    const drop    = prevCtr - currCtr;

    if (curr.impressions >= MIN_IMPRESSIONS && drop >= CTR_DROP_THRESHOLD) {
      drops.push({ query, currCtr, prevCtr, drop,
        impressions: curr.impressions,
        position:    curr.position.toFixed(1)
      });
    }
  });

  if (drops.length === 0) {
    Logger.log('✅ No CTR drops detected this week.');
    return;
  }

  // Sort by largest drop first
  drops.sort((a,b) => b.drop - a.drop);

  // Build email body
  let body = `GSC CTR DROP ALERT\n
Site: ${SITE_URL}\nPeriod: ${thisWeek.start}${thisWeek.end}\n
vs. prior week: ${lastWeek.start}${lastWeek.end}\n
Threshold: -${CTR_DROP_THRESHOLD}pp CTR, min ${MIN_IMPRESSIONS} impressions\n\n
QUERIES FLAGGED (${drops.length}):\n${'─'.repeat(60)}\n`;

  drops.forEach(d => {
    body += `\n"${d.query}"\n
  CTR: ${d.prevCtr.toFixed(1)}% → ${d.currCtr.toFixed(1)}%  (-${d.drop.toFixed(1)}pp)\n
  Impressions: ${d.impressions}  |  Avg Pos: ${d.position}\n`;
  });

  GmailApp.sendEmail(
    ALERT_EMAIL,
    `🚨 GSC CTR Alert: ${drops.length} queries dropped — ${SITE_URL}`,
    body
  );
  Logger.log('📧 Alert sent: %s drops found.', drops.length);
}

14. Multi-Property Pipeline

For agencies managing multiple client properties, the following pattern iterates across an array of property URLs — writing each to its own named sheet tab within a single Sheets file.

Multi-property loop — one tab per client
Apps Script / JS
/**
 * Pulls GSC data for multiple properties and writes each
 * to its own sheet tab. Add client properties to PROPERTIES array.
 */
function exportAllClients() {
  const PROPERTIES = [
    { url: 'https://client-one.com/',    label: 'Client One'  },
    { url: 'https://client-two.com/',    label: 'Client Two'  },
    { url: 'sc-domain:client-three.com', label: 'Client Three' }
  ];

  const service = getSearchConsoleService();
  if (!service.hasAccess()) { Logger.log('Not authorized.'); return; }

  const today     = new Date();
  const endDate   = formatDate(new Date(today - 2  * 86400000));
  const startDate = formatDate(new Date(today - 29 * 86400000));
  const ss        = SpreadsheetApp.getActiveSpreadsheet();

  PROPERTIES.forEach(prop => {
    Logger.log('Processing: %s', prop.label);

    // Get or create tab for this client
    let sheet = ss.getSheetByName(prop.label);
    if (!sheet) sheet = ss.insertSheet(prop.label);
    sheet.clearContents();

    // Fetch data for this property
    const endpoint =
      'https://searchconsole.googleapis.com/webmasters/v3/sites/' +
      encodeURIComponent(prop.url) + '/searchAnalytics/query';

    const response = UrlFetchApp.fetch(endpoint, {
      method: 'POST',
      headers: { 'Authorization': 'Bearer ' + service.getAccessToken(),
                 'Content-Type':  'application/json' },
      payload: JSON.stringify({ startDate, endDate,
        dimensions: ['query'], rowLimit: 500 }),
      muteHttpExceptions: true
    });

    const data = JSON.parse(response.getContentText());
    if (data.error) {
      Logger.log('Error for %s: %s', prop.label, data.error.message);
      return;
    }

    sheet.appendRow(['Query','Clicks','Impressions','CTR','Position']);

    const rows = (data.rows || []).map(r => [
      r.keys[0], r.clicks, r.impressions,
      (r.ctr*100).toFixed(2)+'%', r.position.toFixed(1)
    ]);
    if (rows.length) {
      sheet.getRange(2,1,rows.length,5).setValues(rows);
    }
    Logger.log('  ✅ %s rows written for %s', rows.length, prop.label);

    // Pause between properties to respect rate limits
    Utilities.sleep(1000);
  });
}

15. Troubleshooting

The following errors account for the majority of first-time setup issues. Each has a specific resolution.

Error / SymptomCauseResolution
Not authorized in log Access token not stored — authorization not completed Run authorizeScript(), open the URL in the log, and grant access in the browser prompt.
Error 401: invalid_client Client ID or Client Secret is incorrect in Script Properties Verify values in Script Properties match exactly what's shown in Google Cloud Console → Credentials. No trailing spaces.
Error 403: insufficientPermissions Authenticated user lacks GSC access to the property URL Confirm the Google account used for authorization has at least Restricted User access to the property in Search Console.
Error 403: siteNotOwned Property URL format mismatch Ensure the SITE_URL in your script matches the property URL exactly as it appears in GSC — including trailing slash and protocol. Use sc-domain:yourdomain.com for domain properties.
redirect_uri_mismatch Redirect URI in Google Cloud doesn't match script ID Update the redirect URI in Cloud Console → Credentials to include your actual Script ID: https://script.google.com/macros/d/YOUR_SCRIPT_ID/usercallback
Error 429: rateLimitExceeded API quota exceeded (1,200 req/min or 2,000 req/day) Add Utilities.sleep(500) between requests in pagination loops. If hitting daily quota, spread requests across multiple Cloud projects or reduce request frequency.
Empty rows array in response Date range has no data (too recent or too old) GSC data lags 2–3 days. The 16-month window is approximate — some properties have less. Test with a known date range first using the GSC web UI.
Script times out after 6 minutes Too many rows for free account time limit Reduce rowLimit, split into smaller date range batches, or upgrade to a Google Workspace account (30-minute limit).
OAuth token expires and script fails in trigger Refresh token not obtained during initial authorization Ensure access_type: 'offline' and prompt: 'consent' are in the getSearchConsoleService() function. Run authorizeScript() again to re-authorize and obtain a fresh refresh token.

16. FAQ

What is the Google Search Console API?+
The Google Search Console API is a REST API that provides programmatic access to the data available in the Google Search Console web interface — including search performance metrics (clicks, impressions, CTR, position), index coverage status, URL inspection results, and sitemap data. It enables automated retrieval of this data into custom dashboards, reporting pipelines, and analysis tools without manual CSV export.
What is Google Apps Script?+
Google Apps Script is a cloud-based JavaScript platform built into the Google Workspace ecosystem that enables automation of Google products — including Sheets, Docs, Gmail, Drive, and external APIs. It runs on Google's servers, requires no local infrastructure, and can be triggered on schedules or events. For SEO teams, it is the lowest-friction path to automating Search Console data retrieval into Google Sheets with no cost and no server setup.
Do I need a Google Cloud project to use the Search Console API?+
Yes. The Search Console API requires a Google Cloud project with the API enabled and OAuth 2.0 credentials configured. The project can be on the free tier — the Search Console API has no direct cost for typical usage volumes. Apps Script projects linked to a Google Cloud project inherit its OAuth configuration, enabling authenticated API calls without external server infrastructure.
What data can the Search Console API return?+
The Search Console API's Search Analytics endpoint returns clicks, impressions, CTR, and average position data — dimensioned by query, page, country, device, search type (web, image, video, news), and date. It supports up to 16 months of historical data with a maximum of 25,000 rows per request. Additional endpoints cover Index Coverage, URL Inspection, Sitemaps, and Mobile Usability data.
What is the Search Console API row limit?+
The Search Console API returns a maximum of 25,000 rows per request. For sites with more than 25,000 unique query-page combinations in the reporting period, pagination is required using the startRow parameter to retrieve subsequent batches. Each batch is a separate API call. The API has a quota of 1,200 requests per minute per user and 2,000 requests per day per Google Cloud project.
How do I automate Search Console reports with Apps Script?+
Search Console reports are automated in Apps Script using time-based triggers. After writing a function that pulls data from the Search Console API and writes it to Google Sheets, navigate to the Triggers section in Apps Script, add a new trigger for that function, set the trigger type to Time-driven, and configure the execution schedule (daily, weekly, or monthly). Apps Script will then run the function automatically on that schedule without any manual intervention.
What's the difference between a URL-prefix property and a domain property in the API?+
In Google Search Console, properties come in two formats. A URL-prefix property (e.g., https://www.yourdomain.com/) covers only URLs matching that exact prefix and protocol. A domain property (e.g., sc-domain:yourdomain.com) covers all subdomains and protocols under the domain. In API requests, use the exact property URL format as it appears in your Search Console property list. Domain properties use the sc-domain: prefix in the API endpoint — they are not prefixed with https://.
Why does GSC data lag 2-3 days?+
Google processes and aggregates search performance data with a processing delay of approximately 2 to 3 days before it appears in Search Console. This means the most recent data available in the API is always 2-3 days behind the current date. All date range calculations in your scripts should account for this lag — the end date of your range should be set to at least 2 days before today's date to avoid empty or incomplete data for recent days.

Automate Your GSC Reporting Pipeline

The scripts in this guide are the foundation. The Search Intelligence Hub takes this further — automated entity monitoring, AI Overview citation tracking, and multi-property GEO reporting in a unified dashboard.

Schedule Technical Briefing