DEV Community

Hackceleration
Hackceleration

Posted on • Originally published at hackceleration.com

Building an SEO Position Tracker with Google Search Console API and n8n

You're tracking keyword rankings manually in Google Search Console. Every morning: log in, filter by query, select date range, export to CSV, paste into a spreadsheet. Repeat for each keyword. By Friday, you've spent hours collecting data that should take minutes.

This n8n workflow connects directly to the Google Search Console API, fetches position and impression data for configured keywords, and updates a Google Sheets tracking spreadsheet automatically. Here's how to architect this integration in n8n.

Architecture Overview

The workflow follows this data flow:

1. Read keyword/country config from Google Sheets
2. Read date range from Google Sheets
3. Loop through dates (one at a time)
4. For each date:
   - Query Search Console API with keyword filters
   - Check if position data exists
   - Update tracking sheet with position + impressions
5. Move to next date
Enter fullscreen mode Exit fullscreen mode

Why this architecture? Processing one date at a time prevents API rate limit issues. Google Search Console has quotas, and batch processing can trigger throttling. The IF node handles missing data gracefully—if a keyword has no impressions for a date, the workflow skips that update without crashing.

Alternative considered: Querying all dates in a single API call. Rejected because: (1) harder to handle partial failures, (2) less granular error tracking, (3) complicates the update logic when matching dates to spreadsheet rows.

Google Search Console API Integration

Authentication: OAuth2 Setup

The Search Console API requires OAuth2 credentials. You'll need:

  1. A project in Google Cloud Console
  2. Search Console API enabled
  3. OAuth2 credentials with scope: https://www.googleapis.com/auth/webmasters.readonly
  4. Credentials configured in n8n (Settings → Credentials → Google Search Console OAuth2 API)

API Request Structure

The core API call is an HTTP POST to the searchAnalytics endpoint:

POST https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query

Headers:
  Authorization: Bearer {oauth_token}
  Content-Type: application/json

Body:
{
  "startDate": "2025-02-06",
  "endDate": "2025-02-06",
  "searchType": "web",
  "dimensions": ["query"],
  "dimensionFilterGroups": [{
    "filters": [
      {"dimension": "query", "expression": "exact keyword", "operator": "equals"},
      {"dimension": "country", "expression": "usa", "operator": "equals"}
    ]
  }]
}
Enter fullscreen mode Exit fullscreen mode

Critical parameter: The site URL in the endpoint must be URL-encoded. For https://example.com/, use https%3A%2F%2Fexample.com%2F. Find your exact property URL in Search Console settings.

API Response Structure

When position data exists:

{
  "rows": [
    {
      "keys": ["your keyword"],
      "position": 12.5,
      "impressions": 847,
      "clicks": 23,
      "ctr": 0.027
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

When no data exists (keyword had no impressions):

{}
Enter fullscreen mode Exit fullscreen mode

Handling missing data: The workflow uses an IF node to check $json.rows[0].position. If this field doesn't exist, it skips the Google Sheets update and continues to the next date. This prevents errors when tracking new keywords or low-impression queries.

n8n Configuration Deep-Dive

Google Sheets Integration

Configuration sheet structure:

  • Sheet 1 ("keyword & country"): columns keyword, country
  • Sheet 2 ("dates"): column date in YYYY-MM-DD format
  • Sheet 3 ("position"): columns date, position, impressions

Reading configuration:

// Node: Google Sheets (Get Rows)
Resource: Sheet Within Document
Operation: Get Row(s)
Document: [Your spreadsheet]
Sheet: "keyword & country"
// Returns array of {keyword: "...", country: "..."}
Enter fullscreen mode Exit fullscreen mode

Updating position data:

// Node: Google Sheets (Update Row)
Operation: Update Row
Column to match on: "date"
Values to update:
  - date: {{ $('Loop Through Dates').item.json.date }}
  - position: {{ $json.rows[0].position }}
  - impressions: {{ $json.rows[0].impressions }}
Enter fullscreen mode Exit fullscreen mode

Why Update Row instead of Append? Update Row allows re-running the workflow for the same dates without creating duplicates. Pre-populate your position sheet with dates, and the workflow will fill in position/impression data as it runs.

Split In Batches Configuration

// Node: Split In Batches
Batch Size: 1
// Processes one date per iteration
// Loops back to HTTP Request node until all dates processed
Enter fullscreen mode Exit fullscreen mode

Rate limiting strategy: With batch size 1, you're making ~1 API request per second. Search Console's quota is generous (thousands of queries per day), but processing one at a time keeps you safely under limits. For large backlogs (90+ days), add a Wait node with 1-second delay after each API call.

Implementation Gotchas

Search Console data delay: Search Console finalizes data 2-3 days after the date occurs. If you query yesterday's data, you'll get incomplete results. Always track dates that are at least 3 days old.

Credential scope errors: If you see "Insufficient authentication scopes", your OAuth credential is missing the webmasters scope. Recreate the credential in n8n with the correct scope, then re-authenticate.

Empty responses vs. API errors: The workflow differentiates between "no data for this keyword" (empty response, status 200) and "API error" (status 400/500). The IF node only handles empty responses. API errors will fail the workflow—check your property URL encoding and credential scopes.

Date format mismatches: The Search Console API requires YYYY-MM-DD format. Google Sheets might auto-format dates as MM/DD/YYYY. Use a Set node to transform dates before passing them to the API:

// Node: Set (Format Date)
Value: {{ $json.date_raw.split('/').reverse().join('-') }}
// Converts 02/09/2025 to 2025-02-09
Enter fullscreen mode Exit fullscreen mode

Missing position field vs. null position: The API returns no rows array when a keyword has zero impressions. It returns rows[0].position: null when position data exists but is unavailable (rare). The IF node condition does not exist catches both cases.

Prerequisites

Required accounts:

  • n8n instance (self-hosted or n8n Cloud)
  • Google Search Console access with verified property
  • Google Sheets account
  • Google Cloud project with Search Console API enabled

API credentials:

  • OAuth2 client ID and secret from Google Cloud Console
  • Credential configured in n8n: Settings → Credentials → Google Search Console OAuth2 API
  • Scope: https://www.googleapis.com/auth/webmasters.readonly

Google Sheets template:

  1. Sheet "keyword & country" with columns: keyword, country
  2. Sheet "dates" with column: date (YYYY-MM-DD format)
  3. Sheet "position" with columns: date, position, impressions

Pre-populate dates sheet with your tracking period (e.g., last 30 days). Pre-populate position sheet with matching dates (empty position/impressions columns).

Estimated API costs: Google Search Console API is free with generous quotas (thousands of queries per day). No usage fees.

Get the Complete n8n Workflow Configuration

This tutorial covers the API integration architecture and core concepts. For the complete n8n workflow JSON with all node configurations, Google Sheets template, and a video walkthrough of the setup process, check out the full implementation guide.

Top comments (0)