Table of Contents
Endpoint Architecture
Request Parameters Deep Dive
Payload Structure
Headers Reference
GET Request Method
POST Request Method
Batch Submission Strategy
Error Handling
Response Codes Complete Reference
Retry Logic and Exponential Backoff
Rate Limiting
Security Best Practices
Working Code Examples
Fig 8.1: API payload schematic illustrating the structure of HTTP POST JSON requests, depicting parameter requirements (host, key, keyLocation, and urlList) and response code flows.
Endpoint Architecture
Central Endpoint (Recommended)
plaintext
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark https://api.indexnow.org/indexnow
Submitting to the central endpoint automatically distributes URLs to all participating search engines. This is the recommended approach for all implementations.
Individual Search Engine Endpoints
Engine Endpoint When to Use Central https://api.indexnow.org/indexnowAlways preferred Bing https://www.bing.com/indexnowTesting, debugging Yandex https://yandex.com/indexnowTesting, debugging Naver https://searchadvisor.naver.com/indexnowTesting, debugging Seznam https://search.seznam.cz/indexnowTesting, debugging Yep https://yep.com/indexnowTesting, debugging
Important: You never need to submit to multiple endpoints. One submission to any endpoint reaches all engines.
Request Parameters Deep Dive
Required Parameters (POST)
Parameter Type Constraints Description hoststring Valid hostname, no protocol The fully qualified domain name keystring Min 8 chars, recommended 32+ hex Your verification key keyLocationstring Valid HTTPS URL Full URL to your key file urlListarray 1-10,000 items Array of absolute URLs to submit
Required Parameters (GET)
Parameter Type Constraints Description urlstring Valid HTTPS URL Single URL to submit keystring Min 8 chars Your verification key
Parameter Validation Rules
plaintext
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark host:
- Must be a valid hostname
- No protocol (https://)
- No path (/)
- No port (:8080)
- Examples: "example.com", "subdomain.example.com"
key:
- Minimum 8 characters
- Recommended: 32-64 hexadecimal characters
- Can contain: a-z, A-Z, 0-9, -, _
- Must match the content of your key file exactly
keyLocation:
- Must use HTTPS protocol
- Must be accessible without authentication
- Must return Content-Type: text/plain
- Must return HTTP 200 OK
- File content must match the key parameter exactly
urlList:
- Array of 1 to 10,000 strings
- Each must be a valid absolute URL
- Each must use HTTPS protocol
- Each must belong to the specified host
- Maximum 2,048 characters per URL
- Must be URL-encoded if containing special characters
Payload Structure
Standard POST Payload
json
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark {
"host" : "example.com" ,
"key" : "8f7d6e5c4b3a2918f7d6e5c4b3a2918f" ,
"keyLocation" : "https://example.com/8f7d6e5c4b3a2918f7d6e5c4b3a2918f.txt" ,
"urlList" : [
"https://example.com/page1" ,
"https://example.com/page2" ,
"https://example.com/page3"
]
}
Invalid Payload Examples
json
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark // WRONG: Host includes protocol
{
"host" : "https://example.com" ,
"key" : "abc123" ,
"keyLocation" : "https://example.com/abc123.txt" ,
"urlList" : [ "https://example.com/page" ]
}
// WRONG: URL uses HTTP instead of HTTPS
{
"host" : "example.com" ,
"key" : "abc123" ,
"keyLocation" : "https://example.com/abc123.txt" ,
"urlList" : [ "http://example.com/page" ]
}
// WRONG: URL belongs to different host
{
"host" : "example.com" ,
"key" : "abc123" ,
"keyLocation" : "https://example.com/abc123.txt" ,
"urlList" : [ "https://other-site.com/page" ]
}
// WRONG: Empty urlList
{
"host" : "example.com" ,
"key" : "abc123" ,
"keyLocation" : "https://example.com/abc123.txt" ,
"urlList" : []
}
Header Value Purpose Content-Typeapplication/json; charset=utf-8Specifies JSON payload with UTF-8 encoding
Header Value Purpose Acceptapplication/jsonIndicates expected response format User-AgentYourAppName/1.0Identifies your application (optional but helpful)
GET Request Method
Syntax
plaintext
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark GET https://api.indexnow.org/indexnow?url={URL}&key={KEY}
Example
bash
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark # Submit a single URL via GET
curl -G "https://api.indexnow.org/indexnow" \
--data-urlencode "url=https://example.com/new-post" \
--data-urlencode "key=abc123def456789ghi012jkl345mno67"
URL Encoding Requirements
The url parameter must be URL-encoded:
Original URL Encoded URL https://example.com/pagehttps%3A%2F%2Fexample.com%2Fpagehttps://example.com/page?foo=barhttps%3A%2F%2Fexample.com%2Fpage%3Ffoo%3Dbar
GET Request Limitations
Limitation Impact Single URL only Cannot batch submit Key visible in URL Less secure (appears in server logs) URL length limit ~2,000 characters maximum
When to Use GET
Quick testing and debugging
Single URL manual submissions
Simple scripts without JSON libraries
Browser-based testing
POST Request Method
Syntax
http
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark POST https://api.indexnow.org/indexnow HTTP / 1.1
Content-Type : application/json; charset=utf-8
{
"host" : "example.com" ,
"key" : "YOUR_KEY" ,
"keyLocation" : "https://example.com/YOUR_KEY.txt" ,
"urlList" : [ "https://example.com/page" ]
}
Example with curl
bash
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark curl -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{
"host": "example.com",
"key": "abc123def456789ghi012jkl345mno67",
"keyLocation": "https://example.com/abc123def456789ghi012jkl345mno67.txt",
"urlList": [
"https://example.com/blog/post-1",
"https://example.com/blog/post-2",
"https://example.com/products/widget"
]
}'
Example with Python requests
python
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark import requests
payload = {
"host" : "example.com" ,
"key" : "abc123def456789ghi012jkl345mno67" ,
"keyLocation" : "https://example.com/abc123def456789ghi012jkl345mno67.txt" ,
"urlList" : [
"https://example.com/blog/post-1" ,
"https://example.com/blog/post-2"
]
}
response = requests . post (
"https://api.indexnow.org/indexnow" ,
json = payload,
headers = { "Content-Type" : "application/json; charset=utf-8" }
)
print ( f "Status: { response.status_code } " )
print ( f "Response: { response.text } " )
Example with JavaScript fetch
javascript
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark const payload = {
host : "example.com" ,
key : "abc123def456789ghi012jkl345mno67" ,
keyLocation : "https://example.com/abc123def456789ghi012jkl345mno67.txt" ,
urlList : [
"https://example.com/blog/post-1" ,
"https://example.com/blog/post-2"
]
};
fetch ( "https://api.indexnow.org/indexnow" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json; charset=utf-8"
} ,
body : JSON .stringify (payload)
})
.then (response => {
console .log ( `Status: ${ response .status } ` );
return response .text ();
})
.then (text => console .log ( `Response: ${ text } ` ));
Batch Submission Strategy
Chunking Large URL Lists
When you have more than 10,000 URLs to submit, chunk them into multiple requests:
python
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark import requests
def submit_large_url_list ( urls , host , key , key_location ):
chunk_size = 10000
total_submitted = 0
for i in range ( 0 , len (urls), chunk_size):
chunk = urls [ i : i + chunk_size ]
payload = {
"host" : host ,
"key" : key ,
"keyLocation" : key_location ,
"urlList" : chunk
}
response = requests . post (
"https://api.indexnow.org/indexnow" ,
json = payload,
headers = { "Content-Type" : "application/json; charset=utf-8" }
)
if response . status_code == 200 :
total_submitted += len (chunk)
print ( f "Batch { i // chunk_size + 1} : Submitted { len (chunk) } URLs" )
else :
print ( f "Batch { i // chunk_size + 1} failed: HTTP { response.status_code } " )
# Log failed URLs for retry
return total_submitted
# Usage
urls = [ f "https://example.com/page- { i } " for i in range ( 25000 ) ]
submit_large_url_list (urls, "example.com" , "your-key" , "https://example.com/your-key.txt" )
Batch Submission Best Practices
Practice Why Implementation Batch related URLs Improves crawl efficiency Group by content type or section Include only changed URLs Prevents abuse flags Filter unchanged URLs before submitting Limit frequency Respects rate limits Do not submit the same URL within 24 hours Log all submissions Enables debugging and auditing Store timestamp, URLs, response code Handle failures gracefully Prevents data loss Queue failed batches for retry
Error Handling
Common Errors and Solutions
Error HTTP Status Cause Solution Invalid JSON 400 Malformed JSON body Validate JSON before sending Missing required field 400 Required field absent Check all required fields Invalid host format 400 Host contains protocol/path Use bare hostname only Key verification failed 403 Key file does not match or inaccessible Verify key file content and URL Key file not found 403 keyLocation returns 404 Ensure file exists at specified path Invalid URL format 400 URL not absolute or not HTTPS Use full HTTPS URLs URL host mismatch 400 URL does not match host parameter Ensure all URLs use specified host URL too long 400 URL exceeds 2,048 chars Shorten URL or use URL parameters Too many URLs 400 urlList > 10,000 items Split into multiple requests Rate limited 429 Too many requests Implement backoff and retry Server error 500 Engine-side issue Retry with exponential backoff
Error Handling Pattern
python
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark import time
import requests
from typing import List
class IndexNowClient :
def __init__ ( self , host : str , key : str , key_location : str ):
self . host = host
self . key = key
self . key_location = key_location
self . endpoint = "https://api.indexnow.org/indexnow"
def submit ( self , urls : List [ str ], max_retries : int = 3 ) -> dict :
payload = {
"host" : self . host ,
"key" : self . key ,
"keyLocation" : self . key_location ,
"urlList" : urls
}
for attempt in range (max_retries + 1 ):
try :
response = requests . post (
self.endpoint,
json = payload,
headers = { "Content-Type" : "application/json; charset=utf-8" },
timeout = 30
)
if response . status_code == 200 :
return {
"success" : True ,
"status" : response . status_code ,
"urls_submitted" : len (urls)
}
elif response . status_code == 429 :
# Rate limited—back off
wait_time = 2 ** attempt * 5 # 5, 10, 20 seconds
print ( f "Rate limited. Waiting { wait_time } s before retry..." )
time . sleep (wait_time)
continue
elif response . status_code in [ 500 , 502 , 503 , 504 ] :
# Server error—retry
if attempt < max_retries :
wait_time = 2 ** attempt * 10 # 10, 20, 40 seconds
print ( f "Server error { response.status_code } . Retrying in { wait_time } s..." )
time . sleep (wait_time)
continue
elif response . status_code == 403 :
# Key validation failed—do not retry
return {
"success" : False ,
"status" : response . status_code ,
"error" : "Key validation failed. Check key file." ,
"urls_submitted" : 0
}
elif response . status_code == 400 :
# Bad request—do not retry
return {
"success" : False ,
"status" : response . status_code ,
"error" : f "Bad request: { response . text } " ,
"urls_submitted" : 0
}
else :
return {
"success" : False ,
"status" : response . status_code ,
"error" : f "Unexpected status: { response . status_code } " ,
"urls_submitted" : 0
}
except requests . RequestException as e :
if attempt < max_retries :
wait_time = 2 ** attempt * 5
print ( f "Request failed: { e } . Retrying in { wait_time } s..." )
time . sleep (wait_time)
else :
return {
"success" : False ,
"error" : str (e),
"urls_submitted" : 0
}
return {
"success" : False ,
"error" : "Max retries exceeded" ,
"urls_submitted" : 0
}
Response Codes Complete Reference
Success Responses
Code Meaning Body Action 200 OK Submission accepted Empty or minimal JSON Success—URLs queued for crawling 202 Accepted Submission accepted (alternative) Empty or minimal JSON Same as 200
Client Error Responses
Code Meaning Common Causes 400 Bad Request Invalid request Malformed JSON, missing fields, invalid URL format, host mismatch, URL too long, too many URLs 403 Forbidden Key validation failed Key file inaccessible, key mismatch, wrong keyLocation 404 Not Found Endpoint not found Wrong URL path 405 Method Not Allowed Wrong HTTP method Using GET on POST-only endpoint or vice versa 413 Payload Too Large Request body too large URL list too large or individual URLs too long 422 Unprocessable Entity Valid JSON but invalid data URLs do not match host, invalid key format 429 Too Many Requests Rate limit exceeded Submitting too frequently
Server Error Responses
Code Meaning Action 500 Internal Server Error Engine server error Retry with exponential backoff 502 Bad Gateway Gateway error Retry with exponential backoff 503 Service Unavailable Service temporarily down Retry with longer backoff 504 Gateway Timeout Timeout Retry with exponential backoff
Retry Logic and Exponential Backoff
Recommended Retry Strategy
plaintext
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark Attempt 1: Immediate submission
└── If 429 or 5xx → Wait 5 seconds → Retry
Attempt 2: After 5-second delay
└── If 429 or 5xx → Wait 10 seconds → Retry
Attempt 3: After 10-second delay
└── If 429 or 5xx → Wait 20 seconds → Retry
Attempt 4: After 20-second delay
└── If 429 or 5xx → Wait 60 seconds → Retry
Attempt 5: After 60-second delay
└── If still failing → Queue for later, alert admin
Implementation
javascript
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark // JavaScript retry implementation
async function submitWithRetry (urls , maxRetries = 4 ) {
const payload = {
host : 'example.com' ,
key : process . env . INDEXNOW_KEY ,
keyLocation : `https://example.com/ ${ process . env . INDEXNOW_KEY } .txt` ,
urlList : urls
};
for ( let attempt = 0 ; attempt <= maxRetries; attempt ++ ) {
try {
const response = await fetch ( 'https://api.indexnow.org/indexnow' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON .stringify (payload)
});
if ( response .status === 200 ) {
return { success : true , attempt : attempt + 1 };
}
if ( response .status === 403 ) {
// Do not retry key validation failures
throw new Error ( `Key validation failed: ${ await response .text () } ` );
}
if ( response .status === 400 ) {
// Do not retry bad requests
throw new Error ( `Bad request: ${ await response .text () } ` );
}
if ( response .status === 429 || response .status >= 500 ) {
if (attempt < maxRetries) {
const delay = Math .pow ( 2 , attempt) * 5000 ; // 5s, 10s, 20s, 40s
console .log ( `Attempt ${ attempt + 1 } failed with ${ response .status } . Retrying in ${ delay } ms...` );
await new Promise (resolve => setTimeout (resolve , delay));
continue ;
}
}
throw new Error ( `Unexpected status: ${ response .status } ` );
} catch (error) {
if (attempt === maxRetries) {
throw new Error ( `Max retries exceeded: ${ error .message } ` );
}
}
}
}
Rate Limiting
Official Rate Limits
As of 2026, IndexNow does not publish specific rate limits. However, best practices suggest:
Guideline Recommendation Per-request limit Maximum 10,000 URLs Frequency Submit only when content actually changes Duplicate prevention Do not resubmit the same URL within 24 hours Batch frequency Space large batches by at least a few minutes Abuse threshold Excessive unchanged URL submissions may reduce crawl priority
Rate Limiting Implementation
python
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark import time
from collections import deque
class RateLimitedIndexNowClient :
def __init__ ( self , host , key , key_location , max_requests_per_minute = 60 ):
self . host = host
self . key = key
self . key_location = key_location
self . max_requests = max_requests_per_minute
self . requests = deque ()
def _check_rate_limit ( self ):
now = time . time ()
# Remove requests older than 60 seconds
while self . requests and self . requests [ 0 ] < now - 60 :
self . requests . popleft ()
if len (self.requests) >= self . max_requests :
sleep_time = 60 - (now - self . requests [ 0 ] )
if sleep_time > 0 :
print ( f "Rate limit reached. Sleeping { sleep_time :.1f } s..." )
time . sleep (sleep_time)
def submit ( self , urls ):
self . _check_rate_limit ()
self . requests . append (time. time ())
import requests
payload = {
"host" : self . host ,
"key" : self . key ,
"keyLocation" : self . key_location ,
"urlList" : urls
}
return requests . post (
"https://api.indexnow.org/indexnow" ,
json = payload,
headers = { "Content-Type" : "application/json; charset=utf-8" }
)
Security Best Practices
Key Security
Practice Implementation Use strong keys 32-64 hex characters (128-256 bits entropy) Store securely Environment variables, secrets managers Never commit to Git Add to .gitignore, use .env files Rotate regularly Annual rotation recommended Separate per domain Do not reuse keys across domains Monitor usage Log all submissions, alert on anomalies
HTTPS Enforcement
All URLs in urlList must use HTTPS
keyLocation must use HTTPS
Never send IndexNow requests over HTTP
python
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark def validate_payload ( payload ):
# Validate host
if not payload . get ( 'host' ) or '/' in payload [ 'host' ] or ':' in payload [ 'host' ]:
raise ValueError ( "Invalid host format" )
# Validate key
if len (payload. get ( 'key' , '' )) < 8 :
raise ValueError ( "Key must be at least 8 characters" )
# Validate keyLocation
if not payload . get ( 'keyLocation' , '' ). startswith ( 'https://' ):
raise ValueError ( "keyLocation must use HTTPS" )
# Validate urlList
urls = payload . get ( 'urlList' , [])
if not urls or len (urls) > 10000 :
raise ValueError ( "urlList must contain 1-10,000 URLs" )
for url in urls :
if not url . startswith ( 'https://' ):
raise ValueError ( f "URL must use HTTPS: { url } " )
if len (url) > 2048 :
raise ValueError ( f "URL too long: { url } " )
if payload [ 'host' ] not in url :
raise ValueError ( f "URL does not match host: { url } " )
return True
Working Code Examples
Complete Python Implementation
python
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark #!/usr/bin/env python3
import os
import time
import secrets
import logging
import requests
from pathlib import Path
from typing import List , Optional
logging . basicConfig (level = logging.INFO)
logger = logging . getLogger ( 'indexnow' )
class IndexNowClient :
def __init__ ( self , host : str , key : Optional [ str ] = None ,
key_file_dir : Optional [ str ] = None ):
self . host = host
self . key = key or self . _generate_key ()
self . key_location = f "https:// { host } / { self . key } .txt"
self . endpoint = "https://api.indexnow.org/indexnow"
self . key_file_dir = Path (key_file_dir or os. getcwd ())
self . _ensure_key_file ()
logger . info ( f "IndexNow client initialized for { host } " )
def _generate_key ( self ) -> str :
return secrets . token_hex ( 32 )
def _ensure_key_file ( self ):
file_path = self . key_file_dir / f " { self . key } .txt"
if not file_path . exists ():
file_path . write_text (self.key)
logger . info ( f "Created key file: { file_path } " )
def submit ( self , urls : List [ str ], max_retries : int = 3 ) -> dict :
if not urls :
return { "success" : False , "error" : "No URLs provided" }
if len (urls) > 10000 :
raise ValueError ( "Maximum 10,000 URLs per request" )
payload = {
"host" : self . host ,
"key" : self . key ,
"keyLocation" : self . key_location ,
"urlList" : urls
}
for attempt in range (max_retries + 1 ):
try :
response = requests . post (
self.endpoint,
json = payload,
headers = { "Content-Type" : "application/json; charset=utf-8" },
timeout = 30
)
if response . status_code == 200 :
logger . info ( f "Submitted { len (urls) } URLs successfully" )
return {
"success" : True ,
"status" : response . status_code ,
"urls_submitted" : len (urls),
"attempts" : attempt + 1
}
elif response . status_code == 429 :
if attempt < max_retries :
delay = 2 ** attempt * 5
logger . warning ( f "Rate limited. Retrying in { delay } s..." )
time . sleep (delay)
continue
elif response . status_code >= 500 :
if attempt < max_retries :
delay = 2 ** attempt * 10
logger . warning ( f "Server error { response.status_code } . Retrying in { delay } s..." )
time . sleep (delay)
continue
else :
return {
"success" : False ,
"status" : response . status_code ,
"error" : response . text ,
"urls_submitted" : 0
}
except requests . RequestException as e :
if attempt < max_retries :
delay = 2 ** attempt * 5
logger . error ( f "Request failed: { e } . Retrying in { delay } s..." )
time . sleep (delay)
else :
return {
"success" : False ,
"error" : str (e),
"urls_submitted" : 0
}
return { "success" : False , "error" : "Max retries exceeded" , "urls_submitted" : 0 }
def submit_single ( self , url : str ) -> dict :
return self . submit ([url])
# Usage example
if __name__ == "__main__" :
client = IndexNowClient ( "example.com" )
# Submit single URL
result = client . submit_single ( "https://example.com/new-post" )
print (result)
# Submit batch
urls = [ f "https://example.com/page- { i } " for i in range ( 100 ) ]
result = client . submit (urls)
print (result)
Complete Node.js Implementation
javascript
Monokai Dracula One Dark Night Owl SynthWave '84 Nord GitHub Dark GitHub Light Solarized Light Cobalt2 Shades of Purple Tokyo Night Gruvbox Dark Gruvbox Light Rosé Pine Rosé Pine Dawn Poimandres Monokai Pro Ayu Dark #!/usr/bin/env node
const axios = require ( 'axios' );
const crypto = require ( 'crypto' );
const fs = require ( 'fs' ).promises;
const path = require ( 'path' );
class IndexNowClient {
constructor (options = {}) {
this .host = options .host;
this .key = options .key || this .generateKey ();
this .keyLocation = `https:// ${ this .host } / ${ this .key } .txt` ;
this .endpoint = options .endpoint || 'https://api.indexnow.org/indexnow' ;
this .keyFileDir = options .keyFileDir || process .cwd ();
this .logger = options .logger || console;
this .ensureKeyFile () .catch (err => this . logger .error (err));
}
generateKey () {
return crypto .randomBytes ( 32 ) .toString ( 'hex' );
}
async ensureKeyFile () {
const filePath = path .join ( this .keyFileDir , ` ${ this .key } .txt` );
try {
await fs .access (filePath);
} catch {
await fs .writeFile (filePath , this .key);
this . logger .log ( `Created key file: ${ filePath } ` );
}
}
async submit (urls , maxRetries = 3 ) {
const urlArray = Array .isArray (urls) ? urls : [urls];
if ( urlArray . length === 0 ) {
return { success : false , error : 'No URLs provided' };
}
if ( urlArray . length > 10000 ) {
throw new Error ( 'Maximum 10,000 URLs per request' );
}
const payload = {
host : this .host ,
key : this .key ,
keyLocation : this .keyLocation ,
urlList : urlArray
};
for ( let attempt = 0 ; attempt <= maxRetries; attempt ++ ) {
try {
const response = await axios .post ( this .endpoint , payload , {
headers : { 'Content-Type' : 'application/json; charset=utf-8' } ,
timeout : 30000
});
if ( response .status === 200 ) {
this . logger .log ( `Submitted ${ urlArray . length } URLs successfully` );
return {
success : true ,
status : response .status ,
urlsSubmitted : urlArray . length ,
attempts : attempt + 1
};
}
} catch (error) {
const status = error . response ?.status;
if (status === 429 || status >= 500 ) {
if (attempt < maxRetries) {
const delay = Math .pow ( 2 , attempt) * 5000 ;
this . logger .warn ( `Attempt ${ attempt + 1 } failed ( ${ status } ). Retrying in ${ delay } ms...` );
await new Promise (resolve => setTimeout (resolve , delay));
continue ;
}
}
return {
success : false ,
status : status ,
error : error . response ?.data || error .message ,
urlsSubmitted : 0
};
}
}
return { success : false , error : 'Max retries exceeded' , urlsSubmitted : 0 };
}
async submitSingle (url) {
return this .submit ([url]);
}
}
// Usage
const client = new IndexNowClient ({ host : 'example.com' });
client .submitSingle ( 'https://example.com/new-post' )
.then (result => console .log (result));
client .submit ([
'https://example.com/page-1' ,
'https://example.com/page-2'
]) .then (result => console .log (result));
Key Takeaways
✅ Use POST for production (supports batching, more secure)
✅ Use GET only for testing single URLs
✅ Submit to api.indexnow.org for automatic distribution to all engines
✅ Maximum 10,000 URLs per request —chunk larger lists
✅ All URLs must be HTTPS and belong to the specified host
✅ Key file must be at domain root , accessible, and contain only the key
✅ Implement retry logic with exponential backoff for 429 and 5xx errors
✅ Do not retry 400 or 403 —fix the underlying issue instead
✅ Rate limit yourself —do not submit unchanged URLs repeatedly
✅ Log everything for debugging and auditing
✅ Validate payloads before sending to catch errors early
Previous: Part 7: Complete Technical Implementation Guide
Next: IndexNow Complete Guide (Series Hub)
Related Articles Deepen your understanding with these curated continuations.
View All Articles IndexNow Implementation Guide: WordPress, Shopify, Wix & Custom Step-by-step implementation guide for IndexNow. Learn how to set up IndexNow on WordPress, Shopify, Wix, Squarespace, and Cloudflare.
Which Search Engines Support IndexNow? Complete Guide Find out which search engines support IndexNow, how they share submitted URLs, and how it impacts AI search engines like ChatGPT.
Disadvantages of IndexNow: Limits, Risks & Realities A balanced review of IndexNow limitations: lack of ranking influence, no indexing guarantees, and Google's missing support.