WB × The Multiplier / Web & development
Guide
Automating Search Console, IndexNow, and GA4 for a Small Site (Service Account Edition)
Google retired the sitemap ping in 2023. What replaced it is an API, one service-account email, and fewer than 200 lines of Python and bash.
By Weston BeckwithUpdated 11 min read

Manual URL submission stops scaling around the third deploy. As of September 2026, the supported ways to hand Google a sitemap are a Sitemap: line in robots.txt, the Search Console interface, or the Search Console API; the ping endpoint that scripts relied on was retired in 2023. IndexNow covers Bing and the engines that share its index, but Google does not participate, so a small site needs both.
The pattern I use is one Google Cloud service account, added as a user to Search Console, GA4, and Tag Manager, driving four short scripts: submit the sitemap and read its status, inspect URLs, create GA4 key events, build and publish GTM tags, and push every sitemap URL to IndexNow. I wrote them for The Outpost, a programmatic SEO publication WBX Creative built for Power Lodge Florida, which launched to production on September 17, 2026 with a 92-piece content plan behind it and nobody willing to paste URLs into a form. The IndexNow script also runs on my own site. Everything below is from those files.
How to think about it
A service account is a user, not a magic key. It has an email address ending in iam.gserviceaccount.com, and each Google product treats it exactly like a colleague you invited: it can do what its role allows and nothing else. So the setup is two-sided. In Google Cloud you create the account and download its JSON key. In each product you add the email with the least role that does the job: Full in Search Console, Editor in GA4, Publish in Tag Manager.
Scopes are the other half. Every API call declares what it intends to do (webmasters, analytics.edit, tagmanager.publish), and a key with the wrong scope fails even when the role is right. My scripts pin the scope list at the top of each file so the intent is readable.
The key file lives outside the repo. The Outpost scripts read .secrets/outpost-sa.json two directories up and exit with a plain message if it is missing. A service-account key in a public repo is a real leak; an IndexNow key is not, which I will come back to.
What this pattern does not use: the Google Indexing API. It only accepts pages with JobPosting or BroadcastEvent structured data, requires the service account to be an Owner, and defaults to 200 requests a day. For a normal site it is the wrong tool, and asking for it is a tell.
Step 1: Create the account and add it everywhere
In Google Cloud, create a project, enable the Search Console API, the Google Analytics Admin API, and the Tag Manager API, create a service account, and download a JSON key. Then, in each product:
| Product | Where | Role | Why that role |
|---|---|---|---|
| Search Console | Settings, Users and permissions, Add user | Full | Full can submit sitemaps and inspect URLs; Restricted is mostly view; Owner only for ownership changes |
| GA4 | Admin, Property access management | Editor | Needed to create key events and data streams; Administrator only if the script will manage users |
| Tag Manager | Admin, User management, then container permission | Publish | Edit and Approve can create a version but cannot publish it |
The shared auth module, scripts/google/_auth.py, is a few lines: load the key, build a client for a named API and version with the scopes the caller passes.
from google.oauth2 import service_account
from googleapiclient.discovery import build
KEY = os.path.join(os.path.dirname(__file__), '..', '..', '.secrets', 'outpost-sa.json')
def creds(scopes):
return service_account.Credentials.from_service_account_file(KEY, scopes=scopes)
def svc(name, version, scopes):
return build(name, version, credentials=creds(scopes), cache_discovery=False)
Step 2: Submit the sitemap and read back its status
gsc.py lists every property the service account can see, with its permission level, and stops with a plain instruction if the target site is not among them. That check is the most useful line in the file, because the failure mode of a mis-added user is silence. Then it submits the sitemap and prints what Search Console knows about it.
wm = svc('searchconsole', 'v1', ['https://www.googleapis.com/auth/webmasters'])
wm.sitemaps().submit(siteUrl=SITE, feedpath=SITE + 'sitemap-index.xml').execute()
for s in wm.sitemaps().list(siteUrl=SITE).execute().get('sitemap', []):
print(s['path'], s.get('lastSubmitted'), s.get('isPending'), s.get('errors'))
Two details. siteUrl is either the URL-prefix property (https://host/) or the domain property (sc-domain:host), and it has to match how the property was verified. And the submit call returns an empty body on success, so the list call afterward is how you confirm it landed. The Outpost sitemap is sitemap-index.xml because Astro's integration emits an index plus chunks; my Next.js site emits a single sitemap.xml. Point the script at whichever your build produces.
Quota is not a concern here: sitemap methods and most other Search Console resources allow 20 queries a second per user.
Step 3: Which URLs did Google actually index?
The same script has an inspect mode that takes paths on the command line and calls the URL Inspection method for each, printing the verdict, the coverage state, and the last crawl date.
r = wm.urlInspection().index().inspect(
body={'inspectionUrl': SITE.rstrip('/') + path, 'siteUrl': SITE}).execute()
ir = r['inspectionResult']['indexStatusResult']
print(path, ir.get('verdict'), ir.get('coverageState'), ir.get('lastCrawlTime'))
This is the call that replaces refreshing the Pages report. It answers, per URL, whether Google has fetched the page, whether it is indexed, and which canonical Google chose, which is the only place you find out that Google disagreed with you. The quota is 2,000 inspections a day and 600 a minute per site, generous for a small site checked after each deploy and not enough for a daily sweep of a large one. I run it on the handful of pages that changed.
Step 4: What does IndexNow cover, and what does it not?
IndexNow is a protocol, not a Google product. You generate a key of 8 to 128 characters (letters, digits, hyphens), host it as a text file at https://host/<key>.txt, and either GET a single URL or POST a JSON body with up to 10,000 URLs to any participating endpoint. One submission is shared with every participating engine: Bing, Naver, Seznam, Yandex, and Yep on the home page, with Amazon listed as an endpoint in the FAQ. Google does not participate.
The key is not a secret. Its job is to prove you control the host, and it does that by being publicly fetchable at the location you claim. On my site the key file is committed to public/, and the script reads the key from .env.local only so the file name and the request body agree.
scripts/indexnow.sh pulls every loc from the live sitemap (or takes paths as arguments), builds one JSON body, and posts it:
URLS=$(curl -s "$H/sitemap.xml" | grep -o '<loc>[^<]*</loc>' | sed 's#</\?loc>##g')
BODY=$(python3 -c "
import json,sys; urls=[u for u in sys.stdin.read().split() if u]
print(json.dumps({'host':'www.wbxcreative.com','key':'$KEY','keyLocation':'$H/$KEY.txt','urlList':urls}))" <<<"$URLS")
curl -s -w "%{http_code}" -X POST -H "Content-Type: application/json; charset=utf-8" \
--data "$BODY" https://api.indexnow.org/indexnow
Read the status code. 200 means accepted, 202 means accepted pending a key check, 400 is a malformed request, 403 is a bad key, 422 means the URLs do not belong to the host you named, and 429 means you have been flagged as spam and should honor the Retry-After header. The protocol asks you to wait at least five minutes before resubmitting a URL and to submit only on a meaningful change, so the full-sitemap mode is for launches and the per-path mode is for everything after.
A 200 is an acknowledgement, not an index entry. Bing Webmaster Tools is where you see what Bing did with the list. Bing's own submission API also exists, takes up to 500 URLs per batch, and Microsoft retired its legacy SOAP and POX endpoints on August 31, 2026, so use the REST ones if you go that way. IndexNow is the simpler path and the one I use.
Step 5: Create GA4 key events from the Admin API
ga4.py finds the property behind a measurement ID by walking account summaries and their data streams, because the measurement ID is the one identifier visible in the site's HTML and it appears nowhere in the Admin API's property path. Then it lists existing key events and, with --apply, creates the ones the site pushes.
ga = svc('analyticsadmin', 'v1beta', ['https://www.googleapis.com/auth/analytics.edit'])
for ev in ('showroom_click', 'phone_click'):
ga.properties().keyEvents().create(
parent=prop['property'],
body={'eventName': ev, 'countingMethod': 'ONCE_PER_EVENT'}).execute()
It also lists Google Ads links and says plainly when there are none, because importing conversions into Ads is the usual next request and it fails without the link. The Admin API is v1beta for the stable surface; quotas are 1,200 requests a minute per project, with writes capped at 600 a minute, none of which a small site will approach.
Step 6: Build and publish the Tag Manager tags
This is the script that saves the most clicking. The site pushes four events to the dataLayer (showroom_click, phone_click, email_click, outbound_click), each with a fixed set of parameters. gtm.py reads the live container version, refuses to run if any of the planned tag names already exist, and then, in a fresh workspace, creates eleven dataLayer variables, four custom-event triggers, and four GA4 event tags that map parameters to variables. By default it is a dry run; --apply creates the workspace, and --publish creates a version and publishes it.
tm = svc('tagmanager', 'v2', [
'https://www.googleapis.com/auth/tagmanager.edit.containers',
'https://www.googleapis.com/auth/tagmanager.edit.containerversions',
'https://www.googleapis.com/auth/tagmanager.publish'])
live = tm.accounts().containers().versions().live(parent=container['path']).execute()
Two things to respect. The Tag Manager API allows 10,000 requests per project per day and a quarter of a request per second, which is 25 per 100 seconds; a script that creates 19 objects and a version sits well under that, but a retry loop does not. And publishing needs the Publish container permission; a service account with Edit will get all the way to a version and then be refused at publish. The --apply stop, which leaves the workspace unpublished so a person can read it in the GTM interface first, is the mode I use on a client's container.
Mistakes I see
- Restricted instead of Full. The sitemap submit fails with a permission error and the person assumes the API is broken. Check the
permissionLevelthe script prints. - Asking for the Indexing API. It is for job postings and live broadcasts. Everything else is sitemap plus inspection.
- Disallowing what you also noindex. Not an API mistake, but URL Inspection is where you discover it: a page blocked by robots.txt cannot deliver its
noindextag. The Next.js SEO checklist has the fix. - Firing IndexNow for every URL on every deploy. The protocol says to wait five minutes and submit on meaningful change. A daily full-sitemap blast from a site whose pages did not change is how you meet the 429.
- Treating the IndexNow key like a secret and the service-account key like a config file. Reversed. The IndexNow key must be public; the JSON key must never touch the repo.
- Publishing from a script on a container someone else maintains. Create the workspace, stop, and let them read it.
- Hardcoding the property. The Outpost scripts pin the site URL, the measurement ID, and the container ID in one module because there is one site. Move those to arguments or environment variables the second there are two.
What it costs / what to expect
Money: there is no per-call price on any of these APIs at this scale; the quotas above are the ceiling, not a meter, and IndexNow has no fee.
Time: an afternoon the first time, most of it in three admin screens adding the email, and seconds per run after that. For a client, this sits inside the technical SEO work on the technical services page, and the GA4 and Tag Manager guide covers what those four events should be for a business that is not a powersports dealer.
Expectations: submission is not indexing. A submitted sitemap gets read on Google's schedule; an inspected URL tells you the truth at that moment; an IndexNow 200 means Bing received a list. None of the three make a thin page rank. What they do is remove the human from a loop that used to be forgotten and give you a verdict per URL instead of a feeling.
Where to go next
The scripts assume the site itself is in order, which means unique titles, canonicals, a real sitemap, and a build that refuses to ship without them; the checklist is that list. The GA4 and Tag Manager guide is the measurement half. And if you want the person who wrote these scripts to point them at your property, my bio covers the rest of the resume, and the contact page is short.
Still pasting URLs into Search Console by hand?
Tell me the site and the stack. The service account and the four scripts are an afternoon, not a project.
Start a conversationQuestions people ask
- How do I submit a sitemap to Google automatically?
- Through the Search Console API. Create a Google Cloud service account, add its email as a Full user on the Search Console property, and call sitemaps.submit with the property URL and the sitemap URL. The old ping endpoint was retired in 2023 and no longer works.
- Does IndexNow work with Google?
- No. As of September 2026 Google does not participate in IndexNow. One submission to api.indexnow.org reaches Bing, Naver, Seznam, Yandex, and Yep, and the IndexNow FAQ lists Amazon as an endpoint as well. For Google you still need the Search Console API and a sitemap.
- Can I use the Google Indexing API to get normal pages indexed faster?
- No. The Indexing API only accepts pages carrying JobPosting or BroadcastEvent structured data, and it comes with a 200-per-day default quota. For everything else, submit a sitemap and use URL Inspection to check status.
- What permission does a service account need in Search Console?
- Full. A Full user can submit sitemaps and run URL inspections; Restricted is mostly read-only. Owner is only needed to change ownership or use the Indexing API. Add the service account under Settings, then Users and permissions, using its iam.gserviceaccount.com email.
- How many URLs can I inspect per day with the Search Console API?
- As of September 2026 the URL Inspection method allows 2,000 queries per day and 600 per minute per site. That is plenty for a small site checked after each deploy, and not enough for a daily sweep of a large one.
Sources
- https://developers.google.com/search/blog/2023/06/sitemaps-lastmod-ping
- https://developers.google.com/webmaster-tools/v1/sitemaps/submit
- https://developers.google.com/webmaster-tools/v1/urlInspection.index/inspect
- https://developers.google.com/webmaster-tools/limits
- https://support.google.com/webmasters/answer/2451999
- https://www.indexnow.org/documentation
- https://www.indexnow.org/faq
- https://learn.microsoft.com/en-us/bingwebmaster/
- https://learn.microsoft.com/en-us/dotnet/api/microsoft.bing.webmaster.api.interfaces.iwebmasterapi.submiturlbatch?view=bing-webmaster-dotnet
- https://developers.google.com/analytics/devguides/config/admin/v1
- https://developers.google.com/analytics/devguides/config/admin/v1/quotas
- https://developers.google.com/tag-platform/tag-manager/api/v2/limits-quotas