How I built a tool to submit sitemaps to Google Search Console in bulk
Search Console only submits sitemaps one at a time, by hand. I built a small Python tool on the Search Console API that submits a whole list in one run, and the setup taught me more than the code.
Project at a glance
- Project
- Bulk sitemap submission to Google Search Console
- Problem
- Search Console only submits sitemaps one at a time, by hand.
- Stack
- Python, Google Search Console API, a Google service account
- How
- Reads a text file of sitemap URLs and submits them all in one run, with retries and a CSV log.
Google Search Console lets you submit sitemaps, but only one at a time, by hand. That's fine for a small site. When you're staring at dozens or hundreds of sitemaps, it turns into a slow, mind-numbing afternoon of copy and paste.
I ran into exactly this, so I built a small Python tool that reads a list of sitemap URLs and submits them all in one run. Here's how it works, the code behind it, and the setup traps that cost me more time than the code itself.
The problem
- Search Console has no bulk-upload button for sitemaps.
- I had a large batch to submit for one property.
- Doing it by hand was slow and easy to get wrong.
Why the easy shortcuts don't work
Before writing any code, I checked the lazy routes:
| Option | Does it work? | Why |
|---|---|---|
| Sitemap “ping” URL | ✕ No | Google retired it. Hitting the URL does nothing now. |
| One sitemap index | ~ Partly | Google discovers every child sitemap from it, but Search Console only shows coverage and error stats per sitemap for sitemaps you submit directly. |
| Search Console API | ✓ Yes | It has a method to submit each sitemap programmatically. |
I wanted the per-file breakdown, so each sitemap had to be submitted on its own. That left the real solution: the Search Console API.
The approach
Three pieces make it work. Put your URLs in a file, run one command, and walk away.
The code
The core is small. The full version is on GitHub; here it's trimmed for reading. First, authenticate as the service account:
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/webmasters"]
def get_service():
creds = service_account.Credentials.from_service_account_file(
"service_account.json", scopes=SCOPES
)
return build("searchconsole", "v1", credentials=creds, cache_discovery=False)Read the sitemap list from a text file, ignoring blank lines and comments:
def load_from_file(path):
with open(path, "r", encoding="utf-8") as f:
return [line.strip() for line in f
if line.strip() and not line.startswith("#")]Submit a single sitemap, with retries for temporary errors:
def submit_one(service, site_url, feedpath):
for attempt in range(1, MAX_RETRIES + 1):
try:
service.sitemaps().submit(
siteUrl=site_url, feedpath=feedpath
).execute()
return "submitted", ""
except HttpError as e:
code = getattr(e.resp, "status", None)
if code in (403, 404):
return "failed", f"HTTP {code} (check owner permission / siteUrl)"
if code in (429, 500, 502, 503) and attempt < MAX_RETRIES:
time.sleep(DELAY_SECONDS * (2 ** attempt))
continue
return "failed", f"HTTP {code}: {e}"
return "failed", "max retries exceeded"This is what that function decides for every sitemap:
submit_one(), decision by decision
Then the main loop ties it together: load the list, submit each sitemap, and pause between requests. The full version also skips anything already submitted, writes a CSV log and can read a sitemap index.
def main():
sitemaps = load_from_file("sitemaps.txt")
service = get_service()
for url in sitemaps:
status, detail = submit_one(service, SITE_URL, url)
print(f"{status}: {url}")
time.sleep(DELAY_SECONDS)How it works, step by step
How the data flows
The script never reads the sitemaps itself. The submit call just tells Google “here's a sitemap, go look at it”, and Google fetches it later, on its own schedule.
Submitted doesn't mean valid
A “submitted” result means Google accepted the request, not that the sitemap is valid. Google fetches and checks the file afterwards, and any errors show up in the Sitemaps report hours later. They're two separate moments:
The traps that cost me the most time
The concept took an hour. The setup took a day, thanks to small, dumb obstacles. If you build this, these will slow you down:
- Hidden file extensions on WindowsRenaming silently created service_account.json.json and sitemaps.txt.txt, so the script couldn't find them.Fix: Turn on “File name extensions” in Explorer before renaming, and check the real names with dir.
- Python not on PATHThe classic. Commands fail even though Python is installed.Fix: Install with “Add to PATH” checked, then open a fresh terminal; open windows don't pick up the change.
- The wrong property formatA URL-prefix property (https://example.com/) and a domain property (sc-domain:example.com) are different objects.Fix: Use the exact property format, or every submission fails with a 403 or 404.
- Owner permission is mandatory“Full” access isn't enough to submit sitemaps.Fix: Add the service account as an Owner of the property.
- Corporate networks and proxiesThe API library uses an older HTTP library that doesn't detect a system proxy, so it can time out on a work network or VPN while the browser works fine.Fix: Route the API through the same proxy your system already uses.
The bigger lesson
The script was the easy part. The real work was everything around it: credentials, permissions, file naming and network quirks. That's true of most automation.
The logic is simple, and the plumbing is where the time goes.
If you're an SEO facing a pile of manual sitemap submissions, this is very doable. Budget your time for the setup, not the code.
nikhilsearch/gsc-sitemap-submitterThe full tool: skip-if-submitted, CSV log and sitemap-index option