A Google Sheets + Apps Script template that checks index status for a set of URLs on a schedule using the Search Console URL Inspection API, stores every result, and reports on it with pivots and charts.
This is not the Google Indexing API. The Indexing API only accepts JobPosting and BroadcastEvent submissions, and its metadata endpoint reports what you submitted — not whether Google has indexed a page.
The endpoint this tool calls:
POST https://searchconsole.googleapis.com/v1/urlInspection/index:inspect
One URL per request. No batch endpoint exists. Inspection uses UrlFetchApp + ScriptApp.getOAuthToken() with scope https://www.googleapis.com/auth/webmasters.readonly. Each user authorises with their own Google account and links their own Cloud project.
GitHub is the source code. Users should never install from here.
Read more about how to use this tool - https://weareroast.com/resources/tools/google-indexing-api-tool/
Then:
- Create or pick your own Google Cloud project (the template’s Cloud project does not copy). Enable Google Search Console API, set the OAuth consent screen, then in this copy: Extensions → Apps Script → Project Settings → Change project and paste your project number. Full steps are under Switch off Default GCP.
- Open Index Checker → Setup.
- Authorise your Google account when asked.
- Pick the Search Console property.
- Import a sitemap or paste URLs.
- Done. The daily trigger is created for you.
That /copy URL looks like:
https://docs.google.com/spreadsheets/d/FILE_ID/copy
Share the template as Anyone with the link → Viewer. /copy puts a private copy in their Drive, with the script already attached. They never see this repo.
Until that link is published, there is no one-click install — Apps Script cannot be installed from GitHub the way a Chrome extension can.
Re-check before treating these as gospel. Google changes them without much notice.
URL Inspection API — official limits
| Limit | Value |
|---|---|
| Inspections per property per day | 2,000 |
| Inspections per property per minute | 600 |
| Per-project (usually irrelevant) | 10,000,000 / day, 15,000 / min |
| Quota reset | Pacific time midnight |
DAILY_CAP defaults to 1,900 to leave headroom.
Apps Script — official quotas
| Limit | Consumer | Workspace |
|---|---|---|
| Script runtime per execution | 6 min | 6 min |
| Trigger total runtime per day | ~90 min | ~6 hr |
| UrlFetch calls per day | ~20,000 | ~100,000 |
| Emails per day | ~100 | ~1,500 |
| Cells per spreadsheet | 10 million | 10 million |
The trigger runtime cap is the real constraint on consumer accounts: 90 minutes of continuation triggers per day limits practical throughput well below 2,000 URLs. Setup warns if the account looks like gmail.com.
mobileUsabilityResult is deprecated on the inspect schema. The sheet still captures Mobile Verdict if present; do not use it in client reporting. The Search Console Mobile Usability report was retired on 1 December 2023.
This is the only “install” work. After this, users get a button.
- Create a blank Google Sheet.
- Extensions → Apps Script. Copy the Script ID from Project Settings.
- Install clasp, then:
cp .clasp.json.example .clasp.json
# paste the Script ID
clasp push- Switch the script off Default GCP (required — see below).
- Run
bootstrapTemplate. Authorise when prompted. - Share → Anyone with the link → Viewer.
- The user link is
https://docs.google.com/spreadsheets/d/FILE_ID/copy— put it in the button above.
Do not share the template as Editor. Viewers who hit /copy get their own sheet and their own script copy; they authorise as themselves.
A Workspace Marketplace add-on would be even easier for users (“Install” from Google), but it needs OAuth verification and a Marketplace listing. The /copy link is the easy path for a public GitHub project.
Default is a hidden Cloud project. You cannot enable APIs on it from Cloud Console. Each /copy gets a new Apps Script project on Default GCP — the template owner’s Cloud project does not copy. Every user creates or links their own standard Cloud project.
- Open Google Cloud Console with the same Google account.
- Create a project (or pick an existing one). Name it e.g.
gsc-index-checker. - Copy the Project number (APIs & Services → Settings, or the project dashboard). That is digits, not the project ID string.
- Enable Google Search Console API: console.cloud.google.com/apis/library/searchconsole.googleapis.com
- APIs & Services → OAuth consent screen:
- User type: Internal (Workspace) or External (gmail).
- App name, user support email.
- If External: Publishing status Testing, add your email under Test users.
- In the Apps Script editor: Project Settings (gear) → Google Cloud Platform Project → Change project → paste the project number.
- Re-run Index Checker → Setup and authorise again (the Cloud project change invalidates the old token).
Until you do this, sites.list and URL Inspection will 403.
For changing the code, not for handing to clients.
- Create a blank Google Sheet → Extensions → Apps Script → copy the Script ID.
cp .clasp.json.example .clasp.jsonand paste that ID.clasp push- Set timezone, enable the Search Console API, run
bootstrapTemplate. - Index Checker → Setup in the sheet.
| Tab | Visibility | Purpose |
|---|---|---|
Settings |
Visible | Named-range backed config |
URLs |
Visible | Input list plus live credit calculator |
Data |
Visible | Append-only result store |
Run Log |
Visible | One row per run |
Pivots |
Visible | Pivot tables over Data |
Dashboard |
Visible | Scorecards and charts |
Glossary |
Visible | Status definitions and API limits |
_Queue |
Hidden | Resumable batch state |
_Quota |
Hidden | Daily quota ledger (Pacific date) |
_Import |
Hidden | Staging area for sitemap wizard |
_Deferred |
Hidden | 429 / trim leftovers requeued for tomorrow |
The script reads Settings by named range, never by A1.
Apps Script kills executions at 6 minutes, so a full daily cap cannot finish in one shot.
buildQueue()takesActive = TRUEURLs whoseNext Dueis today or earlier, sorts by Priority then oldest due first, truncates to remaining quota, writes_Queue, and setsReserved.processQueue()takes a document lock (30s timeout), reads a cursor from Document Properties, inspects the nextBATCH_SIZEURLs inUrlFetchApp.fetchAll()chunks of 10, sleeps ~1.1s between chunks to stay under 600 QPM, appends every result toData, and bumps_Quota.- After 4.5 minutes it persists the cursor and creates a one-off trigger for +1 minute.
- When the queue empties it writes
Run Log, updatesLast Checked/Next Due/Last VerdictonURLs, clearsReserved, deletes continuation triggers, and emails if configured.
| Response | Action |
|---|---|
| 429 | Exponential backoff, 3 attempts, then requeue for tomorrow at Priority 1 |
| 5xx | Exponential backoff, 3 attempts, then log as failed |
| 403 | Abort the entire run — almost always no property access |
| 404 from the API | Logged as a valid inspection result, not a failure |
| URL outside the property | Logged, run continues (no inspect call) |
Keyed on Pacific date, not local date. A 03:00 London run still draws on the previous Pacific day's allowance.
Remaining = DAILY_CAP − Scheduled Used − Ad-hoc Used − Reserved
During the day, Reserved is kept equal to the number of scheduled URLs still due, so ad-hoc runs cannot silently spend tonight's budget. Opening the sheet or View Quota refreshes that reservation.
Index Checker → Run Ad-hoc opens a sidebar. Paste URLs or use the current URLs selection.
If the request fits in Remaining, it runs immediately against Ad-hoc Used.
If not, a modal shows the explicit numbers and three actions:
| Button | Behaviour |
|---|---|
| Trim to fit | Runs what fits, queues the rest for tomorrow at Priority 1 |
| Borrow from tonight | Second confirmation. Releases reservation, runs the request, shrinks tonight's scheduled run, flags Borrowed From Scheduled on the Run Log. Hidden when ADHOC_BORROW_ALLOWED is FALSE |
| Queue for tomorrow | No spend now |
Index Checker → Import from Sitemap
- Search Console —
GET /webmasters/v3/sites/{siteUrl}/sitemaps, then fetch each feed. - Direct URL — paste a sitemap (needed for staging / unsubmitted files).
Parser: sitemap index recursion (depth cap 3), .xml.gz via Utilities.ungzip, <lastmod> captured, malformed XML fails with the sitemap URL named (no half-import). Filter with include/exclude regex before commit. Section is derived from the first path segment and can be remapped. Orphaned URLs are flagged Orphaned = TRUE, never deleted.
Priority from lastmod: ≤7 days → 1, ≤30 days → 2, older or absent → 3.
| Item | What it does |
|---|---|
| Setup | Property picker, consumer-account warning, daily trigger, smoke test |
| Run Now | Build queue + process |
| Run Ad-hoc | Sidebar |
| Import from Sitemap | Wizard |
| Build Queue Only | Write _Queue without inspecting |
| View Quota | Today's Pacific ledger |
| Reset Triggers | Delete all project triggers; recreate the daily one if setup is complete |
| Purge Old Data | Delete Data rows older than DATA_RETENTION_DAYS |
| Rebuild sheet structure | Re-run bootstrapTemplate (does not wipe Data / URLs / Run Log / quota history) |
Setup is guarded by a SETUP_COMPLETE document property so reopening the sheet does not retrigger the wizard.
| Case | Expected |
|---|---|
Empty URLs sheet |
Queue builds with 0 rows, run logs “Nothing due.” |
| URL outside the property | Data row with a clear error, run continues |
| No property access (403) | Run aborts immediately with a readable message |
| 2,500 due vs 1,900 cap | Truncates; oldest overdue + highest priority kept |
| Execution hits 4.5 minutes | Continuation resumes at the stored cursor |
| Manual run during a scheduled run | Lock refuses the second caller |
| Ad-hoc over remaining | Trim / Borrow / Queue tomorrow all work |
| Gzipped sitemap index three levels deep | Recurses to depth 3 |
| 50,000-URL sitemap | Filter step before commit; no dump onto URLs |
| URL removed from sitemap | Orphaned = TRUE, row kept |
| Run spanning Pacific midnight | Later inspections increment the new PT date |
| Retention purge with 200 days of data | Rows older than DATA_RETENTION_DAYS removed |
src/
appsscript.json
00_Constants.gs … 15_Bootstrap.gs
Setup.html, AdhocSidebar.html, QuotaWarning.html,
BorrowConfirm.html, SitemapWizard.html, QuotaView.html, Styles.html
Runtime helpers are named with a trailing _. Those names are not callable from google.script.run; public wrappers live in 13_Menu.gs.