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.
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.
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.
Search Analytics — 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.
Sites — List
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.
URL Inspection
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
Sitemaps — List
Returns all sitemaps submitted for a property with submission date, last download date, and warning/error counts. Useful for automated sitemap health monitoring.
Index Coverage — Issues
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
| Dimension | Description | Example Value | Notes |
|---|---|---|---|
| query | The search query string | "search console api" | Some queries anonymized as "(not provided)" |
| page | The URL that appeared in results | https://yourdomain.com/page/ | Full canonical URL including protocol |
| country | ISO 3166-1 alpha-3 country code | USA, GBR, DEU | Based on user location |
| device | Device category | MOBILE, DESKTOP, TABLET | Values are uppercase strings |
| date | Calendar date | 2026-07-01 | YYYY-MM-DD format |
| searchAppearance | Search feature type | RICH_SNIPPET, AMP, VIDEO | Cannot 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.
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.
-
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. -
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. -
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.
-
Add the required OAuth scope
On the Scopes screen, click Add or Remove Scopes. In the filter field, enter
webmasters. Check the scopehttps://www.googleapis.com/auth/webmasters.readonlyfor read-only data access. If you need URL inspection write access (to submit URLs for indexing), also addhttps://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.
-
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). -
Add the Apps Script redirect URI
Under Authorized redirect URIs, click Add URI and paste the following URI exactly:
Redirect URIURIhttps://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.
-
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.
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
-
Create a new Apps Script project
Navigate to
script.google.comand click New Project. Rename the project from "Untitled project" to something descriptive — e.g.,GSC Search Analytics Automation. The editor will open with a defaultCode.gsfile. -
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 URIURIhttps://script.google.com/macros/d/1BxiMV...AbCdEfGhIjKlMnOpQrStUv/usercallback
-
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.
-
Store credentials in Script Properties
In Apps Script Project Settings, scroll to Script Properties. Add two properties:
Script PropertiesKEY/VALUECLIENT_ID → your-client-id.apps.googleusercontent.com CLIENT_SECRET → your-client-secret-value
-
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 IDID1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDFSelect 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.
/** * 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.
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.
/** * 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
{ "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.
/** * 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); }
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.
/** * 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.
-
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.
-
Configure the trigger settings
In the trigger configuration panel:
Trigger ConfigurationSETTINGSChoose 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 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.
/** * 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.
/** * 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 / Symptom | Cause | Resolution |
|---|---|---|
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
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.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://.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