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.
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.
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.
URL Inspection
Returns index status, crawl status, canonical URL, and mobile usability verdict for a specific URL. 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.
Index Coverage — Issues
Returns index coverage issue categories (Crawled – currently not indexed, Discovered – currently not indexed, etc.) with issue counts.
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.
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.
-
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. -
Enable the Search Console API
In the left sidebar, navigate to APIs & Services → Library. Search for
Google Search Console API. Click the result, then click Enable. The API status will change to "Enabled". -
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, 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:
Redirect URIURIhttps://script.google.com/macros/d/{SCRIPT_ID}/usercallback -
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.
Security Note: Never hardcode your Client ID or Client Secret directly in Apps Script code. Store credentials in Script Properties and reference them via PropertiesService.getScriptProperties().getProperty('CLIENT_ID').
7 Apps Script Project Setup
-
Create a new Apps Script project
Navigate to
script.google.comand click New Project. Rename the project to something descriptive — e.g.,GSC Search Analytics Automation. -
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 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.
-
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:
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF. Select the latest version, set the identifier toOAuth2, and click Add.
8 The Authentication Function
With the OAuth2 library installed and credentials stored in Script Properties, the following three functions establish and manage authentication.
/** * 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') .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth') .setTokenUrl('https://accounts.google.com/o/oauth2/token') .setClientId(props.getProperty('CLIENT_ID')) .setClientSecret(props.getProperty('CLIENT_SECRET')) .setPropertyStore(props) .setCallbackFunction('authCallback') .setScope('https://www.googleapis.com/auth/webmasters.readonly') .setParam('access_type', 'offline') .setParam('prompt', 'consent'); } /** * Handles the OAuth callback. */ 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.' ); } /** * Run this function once to trigger the OAuth authorization flow. */ 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.'); } }
Run authorizeScript once, open the URL from the log, sign in, and click Allow.
Verification: Run authorizeScript again. The log should now print ✅ Already authorized.
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.
/** * Fetches the top 10 queries by clicks for the last 28 days. */ function fetchTopQueries() { const SITE_URL = 'https://www.yourdomain.com/'; const service = getSearchConsoleService(); if (!service.hasAccess()) { Logger.log('Not authorized. Run authorizeScript() first.'); return; } const today = new Date(); const endDate = formatDate(new Date(today - 2 * 86400000)); const startDate = formatDate(new Date(today - 29 * 86400000)); const payload = { startDate, endDate, dimensions: ['query'], rowLimit: 10 }; 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('API Error: %s', JSON.stringify(data.error)); return; } (data.rows || []).forEach(row => { Logger.log( '%-40s | clicks: %-5s | impr: %-6s | ctr: %s% | pos: %s', row.keys[0], 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'); }
API Response Structure
{ "rows": [ { "keys": ["search console api apps script"], "clicks": 142, "impressions": 1840, "ctr": 0.0771, "position": 3.2 } ] }
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'. */ 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.'); return; } const today = new Date(); const endDate = formatDate(new Date(today - 2 * 86400000)); const startDate = formatDate(new Date(today - 29 * 86400000)); const payload = { startDate, endDate, dimensions: ['query', 'page'], 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; } const ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getSheetByName(SHEET_NAME); if (!sheet) sheet = ss.insertSheet(SHEET_NAME); sheet.clearContents(); const headers = [ 'Query', 'Page', 'Clicks', 'Impressions', 'CTR', 'Avg Position', 'Date Range', 'Exported At' ]; sheet.appendRow(headers); const rows = (data.rows || []).map(row => [ row.keys[0], row.keys[1], row.clicks, row.impressions, (row.ctr * 100).toFixed(2) + '%', row.position.toFixed(1), `${startDate} to ${endDate}`, new Date().toISOString() ]); 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 — open it via Extensions → Apps Script from within the Google Sheet. If you created 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.
/** * Fetches ALL query data from GSC using paginated requests. * 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: %s', pageRows.length, allRows.length); if (pageRows.length < PAGE_SIZE) break; startRow += PAGE_SIZE; Utilities.sleep(500); } Logger.log('✅ Total rows: %s across %s pages', allRows.length, page); return allRows; }
12 Scheduling Automated Reports with Triggers
Apps Script triggers run functions on a schedule without manual execution. Setting up a weekly trigger means your GSC data is refreshed in Sheets automatically.
-
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.
-
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. Approve any additional permissions when prompted.
Execution Time Limit: Apps Script functions have a maximum execution time of 6 minutes for free accounts and 30 minutes for Google Workspace. Scripts that paginate through very large datasets may approach this limit. If you hit it, split the export into multiple functions — one per date range segment.
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.
/** * Compares top-query CTR between the current week and last week. * Sends a Gmail alert for any query where: * - Impressions >= MIN_IMPRESSIONS * - 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; const CTR_DROP_THRESHOLD = 2; const service = getSearchConsoleService(); if (!service.hasAccess()) { Logger.log('Not authorized.'); return; } 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)) }; 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); const drops = []; currentMap.forEach((curr, query) => { const prev = previousMap.get(query); if (!prev) return; 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.'); return; } drops.sort((a,b) => b.drop - a.drop); 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.
/** * 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); let sheet = ss.getSheetByName(prop.label); if (!sheet) sheet = ss.insertSheet(prop.label); sheet.clearContents(); 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); 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. |
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. |
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. |
Error 403: siteNotOwned |
Property URL format mismatch | Ensure the SITE_URL matches exactly as it appears in GSC — including trailing slash. 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 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. If hitting daily quota, spread requests across multiple Cloud projects. |
Empty rows array in response |
Date range has no data (too recent or too old) | GSC data lags 2–3 days. 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 Google Workspace (30-minute limit). |
| OAuth token expires in trigger | Refresh token not obtained during initial authorization | Ensure access_type: 'offline' and prompt: 'consent' are in getSearchConsoleService(). Run authorizeScript() again. |
16 FAQ
startRow parameter. The API has a quota of 1,200 requests per minute per user and 2,000 requests per day per project.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