Skip to content
Coding Script or NotebookGoogle ColabFree

IndexNow Bulk Submitter and Search Console Index Checker (Python Scripts)

ForContent auditsSite migrations

By Lazarina Stoy· Updated 24 Oct 2026

Publishing at volume raises two questions every week: have the search engines heard about my new and updated pages, and has Google actually indexed them? This free pair of Python scripts covers both. One submits URLs in bulk through IndexNow; the other checks Google’s index status through the Search Console URL Inspection API.

Who it’s for

Technical SEOs, developers and site owners who publish or update many pages, run migrations, or want a log of what was submitted and when.

How to use it

  1. Install the requirements (locally, or in Google Colab by uploading the script and running it with !python).
  2. Run generate-key to create your IndexNow key and key file, then upload the file to your site root.
  3. Run check-key to confirm search engines can read it. The submit command checks it again before the first batch.
  4. Run submit with --dry-run on your sitemap (add --since to send only recently changed URLs), a crawl export, or a text file of URLs. Review what would be sent.
  5. Start small with --limit 5, then submit for real. Every URL is logged to indexnow_submissions.csv.
  6. Optionally, run check_indexation_gsc.py with a service account that has access to your Search Console property to check which URLs Google has indexed.

What you get

A repeatable submit step you can run after every publish or migration batch, a CSV log of every URL sent (with batch, status and timestamp) that you can chart in Looker Studio, and gsc_inspection.csv with Google’s verdict, coverage state and last crawl time for each URL. If you hit the daily quota, run it again the next day and it picks up where it stopped.

What the submitter protects you from

  • URLs that aren’t on your host (filtered out before sending) and duplicates (removed, order kept).
  • A missing or wrong key file, and keys in the wrong format.
  • Rate limits: on a 429 it waits 30, 60, 120 and 240 seconds before retrying.
  • A failed batch: it stops, so the rest of your list isn’t wasted.

Limits

  • Google doesn’t take part in IndexNow. Bing, Yandex, Seznam, Naver, Yep and others share the URLs you submit to any one endpoint.
  • A 200 or 202 response means the URLs were received. Neither means indexed.
  • The URL Inspection API allows 2,000 inspections per property per day, so check large sites in batches.
  • Both scripts are free; you only need a site you own and, for the checker, a Google Cloud service account.

The full walkthrough, including what changed from my 2022 code, is in The Ultimate Guide to the IndexNow API. If you’re new to running Python for SEO, the Introduction to Machine Learning for SEO course starts from the basics.

What’s included

  • Generates and checks your IndexNow key file before anything is sent
  • Collects URLs from a sitemap index (with a date filter), a crawl export or a text file, removes duplicates and off-host URLs
  • Batches of up to 10,000 URLs, dry-run mode, retries with backoff on 429, stops on a failed batch
  • Logs every URL sent to a CSV you can chart in Looker Studio
  • Optional bulk Google index check through the Search Console URL Inspection API
. Disclosure menus (APG pattern), hover intent, mobile dialog with focus trap, announcement dismiss, skip-link target, newsletter status message. No dependencies. */ (function () { 'use strict'; var d = document;/* ---------- skip link target: #main, else the page's
, else the first block after the header */ (function () { var mains = d.querySelectorAll('main'); if (mains.length > 1) { // Our post-content
plus another one: drop the landmark from ours. var ours = d.querySelector('main[data-mlfc-main]'); if (ours) { ours.setAttribute('role', 'none'); ours.removeAttribute('id'); } } var skip = d.querySelector('.mlfc-skip'); if (!skip) return; var t = d.getElementById('main'); if (!t || (t.getAttribute('role') === 'none')) { t = d.querySelector('main:not([role="none"]), [role="main"]'); if (!t) { var hd = skip.closest('.wp-block-template-part') || skip.closest('.mlfc-hdw'); t = hd && hd.nextElementSibling; } if (!t) return; if (!t.id) t.id = 'main'; skip.setAttribute('href', '#' + t.id); } if (!t.hasAttribute('tabindex')) t.setAttribute('tabindex', '-1'); skip.addEventListener('click', function () { setTimeout(function () { t.focus({ preventScroll: false }); }, 0); }); })();/* ---------- announcement dismiss */ d.querySelectorAll('[data-mlfc-ann]').forEach(function (bar) { var btn = bar.querySelector('[data-mlfc-ann-close]'); if (!btn) return; btn.addEventListener('click', function () { try { localStorage.setItem(bar.getAttribute('data-mlfc-ann'), '1'); } catch (e) {} var next = d.querySelector('.mlfc-logo'); bar.hidden = true; if (next) next.focus(); }); });/* ---------- disclosure dropdowns */ var items = [].slice.call(d.querySelectorAll('.mlfc-hdw [data-mlfc-dd]')); var fine = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; function btnOf(it) { return it.querySelector('button[aria-controls]'); } function panelOf(it) { return d.getElementById(btnOf(it).getAttribute('aria-controls')); } function isOpen(it) { return btnOf(it).getAttribute('aria-expanded') === 'true'; } function setOpen(it, open) { var b = btnOf(it), p = panelOf(it); if (!b || !p) return; b.setAttribute('aria-expanded', open ? 'true' : 'false'); p.hidden = !open; if (open) { // keep the panel inside the viewport p.style.left = ''; p.style.right = ''; var r = p.getBoundingClientRect(); if (r.right > window.innerWidth - 12) { p.style.left = 'auto'; p.style.right = '0'; } } } function closeAll(except) { items.forEach(function (it) { if (it !== except && isOpen(it)) setOpen(it, false); }); }items.forEach(function (it) { var b = btnOf(it), tIn, tOut; if (!b) return; b.addEventListener('click', function () { clearTimeout(tIn); clearTimeout(tOut); var open = !isOpen(it); closeAll(it); setOpen(it, open); }); if (fine) { // Hover intent: open after 300 ms, close 400 ms after the pointer leaves trigger + panel. it.addEventListener('mouseenter', function () { clearTimeout(tOut); tIn = setTimeout(function () { closeAll(it); setOpen(it, true); }, 300); }); it.addEventListener('mouseleave', function () { clearTimeout(tIn); tOut = setTimeout(function () { setOpen(it, false); }, 400); }); } it.addEventListener('focusout', function (e) { if (e.relatedTarget && !it.contains(e.relatedTarget)) setOpen(it, false); }); it.addEventListener('keydown', function (e) { if (e.key === 'Escape' && isOpen(it)) { e.stopPropagation(); setOpen(it, false); b.focus(); } }); }); d.addEventListener('click', function (e) { items.forEach(function (it) { if (isOpen(it) && !it.contains(e.target)) setOpen(it, false); }); }); d.addEventListener('keydown', function (e) { if (e.key !== 'Escape') return; items.forEach(function (it) { if (isOpen(it)) { setOpen(it, false); btnOf(it).focus(); } }); });/* ---------- mobile menu dialog */ var menuBtn = d.querySelector('.mlfc-menu-btn'); var dlg = d.getElementById('mlfc-mnav'); if (menuBtn && dlg) { var FOC = 'a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])'; var openMenu = function () { dlg.hidden = false; menuBtn.setAttribute('aria-expanded', 'true'); d.body.classList.add('mlfc-lock'); var c = dlg.querySelector('[data-mlfc-close]'); if (c) c.focus(); }; var closeMenu = function (restore) { if (dlg.hidden) return; dlg.hidden = true; menuBtn.setAttribute('aria-expanded', 'false'); d.body.classList.remove('mlfc-lock'); if (restore !== false) menuBtn.focus(); }; menuBtn.addEventListener('click', function () { dlg.hidden ? openMenu() : closeMenu(); }); dlg.querySelector('[data-mlfc-close]').addEventListener('click', function () { closeMenu(); }); dlg.addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); closeMenu(); return; } if (e.key !== 'Tab') return; var f = [].filter.call(dlg.querySelectorAll(FOC), function (el) { return el.offsetParent !== null || el === d.activeElement; }); if (!f.length) return; var first = f[0], last = f[f.length - 1]; if (e.shiftKey && d.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && d.activeElement === last) { e.preventDefault(); first.focus(); } }); // In-page links (e.g. #courses) close the menu first. dlg.addEventListener('click', function (e) { var a = e.target.closest && e.target.closest('a[href^="#"]'); if (a) closeMenu(false); }); var mq = window.matchMedia('(min-width: 1024px)'); var onMq = function () { if (mq.matches) closeMenu(false); }; if (mq.addEventListener) mq.addEventListener('change', onMq); else if (mq.addListener) mq.addListener(onMq); }/* ---------- newsletter: plain POST to Kit in a new tab (same pattern as mlfa2_newsletter()); say so in the live region */ d.querySelectorAll('form[data-mlfc-nl]').forEach(function (form) { var msg = form.querySelector('.mlfc-nl__msg'); form.addEventListener('submit', function () { if (msg) msg.textContent = 'Thanks. Kit opens in a new tab: confirm your subscription there and check your inbox.'; }); }); })();