Baobaobaolin.com
date
entry
038
topic
infrastructure
rev
—

Three IndexNow scripts send the whole sitemap by default, and exit 0 when rejected

The same submission logic appears in three of my scripts: no arguments sends the whole sitemap, a 202 prints as "submitted", and a rejection exits 0. Checked against IndexNow's docs and a fake server.

IndexNow is a push protocol: a site sends URLs that were added, updated or deleted to an endpoint (my scripts POST them in JSON batches), and the participating search engines (Bing, Yandex and others) share them with each other. Google isn't one of them, as covered in the note on Search Console's two timestamps.

Each of my four sites has a submission script. Reading all four side by side this time, three turned out to share the same submission logic: the first written on July 16, 2026 (switched from curl to this Python version the same day), then reused on August 22 and October 9, with small differences such as the sitemap path and the arrow in the output, and all three carrying the same three problems. This note covers those three: what gets sent with no arguments, what a 202 gets printed as, and what the exit code is when a submission is rejected; then how the fourth script is written and what I'd change.

What no arguments sends

The template's core is a Python snippet embedded in shell (excerpt; imports and the key declaration omitted):

urls = sys.argv[1:] or re.findall(r'<loc>(.*?)</loc>', open("public/sitemap.xml").read())
payload = {"host": "www.yutuo-tech.com", "key": key,
           "keyLocation": f"https://www.yutuo-tech.com/{key}.txt", "urlList": urls}

Given URLs, it sends those; given none, it sends every <loc> in the sitemap that matches this regex. The first line of the usage notes is the no-argument form, commented "submit every URL in the sitemap". Sizes of the committed sitemaps in the three sites' repos on October 13, 2026:

SiteScript writtenSent with no arguments
yutuo-tech.com2026-07-16413 URLs
An export site2026-08-221,920 URLs
A new site2026-10-0915 URLs

IndexNow's FAQ has a question that is exactly "can I submit all my site's URLs", and the answer is that it's meant for recently added, updated or deleted URLs and "not designed for submitting every URL on your site at once"; if the whole site's content was recently updated, such as after a migration or redesign, submitting everything is acceptable, and ongoing discovery belongs to an XML sitemap. Another answer says to avoid submitting the same URL many times a day unless the content changed meaningfully.

So the template's default doesn't filter for changes, which steers toward the routine whole-site resubmission the FAQ advises against. Suppose that on the site with 1,920 URLs you ran the no-argument version after each new post: what actually changed might be the new post and a few listing pages, but the other nineteen hundred-odd URLs go out too. (I have no record of how many times these three were actually run; this follows from the default.)

A 202 prints as "submitted"

The template's success line:

r = urllib.request.urlopen(req)
print("HTTP", r.status, "→ submitted", len(urls), "URLs")

200 and 202 both come back as normal responses and land here (HTTP errors become HTTPError; connection failures raise URLError and the like), so the status codes differ but the label is "submitted 413 URLs" for both. IndexNow's documentation separates them: 200 is "URL submitted successfully", 202 is "URL received. IndexNow key validation pending". The FAQ adds that a first submission may get a 202 and that the search engine verifies the key before processing the content.

In other words, when you see a 202 the question of whether the key file is right hasn't been answered yet, and the shared "submitted" label doesn't say so. The FAQ's own sample code also treats 200 and 202 as success, printing "URLs submitted successfully". Neither 200 nor 202 means the URLs have been crawled or indexed.

Rejected, and still exit 0

The template catches HTTPError, prints the status and body, and lets the program end normally. I wrote a fake server of about ten lines that returns whatever status it's told and pointed the template's endpoint at it (keeping the submission and error-handling logic, swapping the endpoint, host, key and sitemap input for test values), and sent yutuo-tech.com's 413-URL sitemap (output and exit codes, tidied):

HTTP 200 -> submitted 413 URLs     exit=0
HTTP 202 -> submitted 413 URLs     exit=0
HTTP 403 …                         exit=0
HTTP 422 …                         exit=0
HTTP 429 …                         exit=0
(server not running)               exit=1

In the docs, 403 means the key isn't valid (key file not found, or the key isn't in it), 422 means URLs that don't belong to the host or a key that doesn't match the schema, and 429 is too many requests, potential spam. All three rejections exit 0; in this test only the run with no server gave 1 (other uncaught exceptions, such as a missing sitemap, would also be non-zero). The fake server's response bodies are my own, so this shows the script's control flow, not what IndexNow actually returns.

None of the three sites' deploy scripts call it today (I grepped); and there's no run log to say how it's actually used; if it's run by hand and read off the screen, the risk is in the reading: a single line saying HTTP 403 in a long stretch of output, with exit code 0, can be missed. The day someone wires it into a deploy that only checks exit codes, a failed submission will be judged a success.

All four key files were reachable that day

Since a 403 would be easy to miss, I checked the key files too. IndexNow's docs require a key of 8 to 128 characters, using only upper- and lowercase letters, digits and hyphens; the recommended placement is a UTF-8 {key}.txt at the site root containing the key itself (keyLocation can also point elsewhere on the same host, but then only URLs under that path can be submitted). Fetching all four sites' key files with curl on October 13: every one returned 200 with content matching the file name. That only shows they were reachable that day, not that any search engine has verified them. One detail: yutuo-tech.com's key file lives under www, and the same path on the apex domain returns 301; the template's host and keyLocation both say www, so they line up.

How the fourth script does it

The fourth script, written on September 12, 2026 (for a database site whose sitemap index covers about twenty thousand URLs, most generated from the database), didn't follow the template:

  • Dry run by default. Given URLs, it only lists them unless you add --submit, because notifying an outside service can't be undone; with no URLs at all it exits with an error.
  • No "send everything" option. You give URLs yourself (--url or --stdin), or use --since <git ref> to have git list articles added or modified between that ref and HEAD (deletions and uncommitted changes excluded), mapped to URLs for the languages that changed; database-generated pages don't go this way.
  • The key is read from the file. It looks for the one key file in web/public/ (recognizing only names of 16 to 64 lowercase hex characters, narrower than the protocol allows) and checks that its content matches its name, rather than keeping a second copy in the code.
  • When actually submitting, only 200 and 202 exit 0. Any other status exits 1, and with several batches one failure makes the whole run exit 1.

Its comments include a line saying that submitting unchanged URLs "lowers trust". Re-reading IndexNow's documentation and FAQ this time, I couldn't find that claim; the FAQ says "don't submit many times a day" and "not designed for submitting a whole site at once". That line should be rewritten to match the docs rather than say more than they do.

Three changes for the template: with no arguments, don't read the sitemap; fail and print usage instead. Print a 202 as "received, key validation pending" rather than "submitted". Exit with sys.exit(1) on any status other than 200 or 202. The code below replaces the template from urls = to the end, with the key, payload and req setup kept in the middle; the script path in the usage line differs per site, so adjust it.

urls = sys.argv[1:]          # no more falling back to the sitemap
if not urls:
    sys.exit("usage: bash scripts/indexnow-ping.sh https://… [https://…]\n"
             "This script only sends URLs you name as added, updated or deleted; it doesn't read the sitemap.")
# … (key, payload and req setup as before)
try:
    r = urllib.request.urlopen(req, timeout=30)
    label = {200: "submitted", 202: "received, key validation pending"}.get(r.status)
    print("HTTP", r.status, label or "unexpected", len(urls), "URLs")
    sys.exit(0 if label else 1)
except urllib.error.HTTPError as e:
    print("HTTP", e.code, e.read().decode(errors="replace")[:300])
    sys.exit(1)

If you remember one thing

Copying a template copies its defaults too. Of the three problems in the opening, one defaults to the routine whole-site resubmission the FAQ advises against, one covers "still validating" with a "submitted" label, and one turns a rejection into exit code 0; each of the three scripts carries a copy, and the fourth uses different defaults, output and error handling. I haven't changed the three scripts yet; this note is the comparison before I do.

Revision history

  1. First published