If you publish content frequently and want search engines to discover it faster, IndexNow is one of the easiest wins available. In this tutorial, I'll walk through a complete PHP implementation of the IndexNow protocol, based on what I built for DailyWatch, a video discovery platform that publishes hundreds of new pages daily.
What Is IndexNow?
IndexNow is an open protocol that lets you notify participating search engines (Bing, Yandex, Seznam, Naver) the moment a URL is created, updated, or deleted. Instead of waiting for crawlers to find your changes, you push the information to them.
Step 1: Generate Your API Key
The key can be any string of hexadecimal characters (a-f, 0-9), between 8 and 128 characters:
// Generate a random IndexNow API key
$apiKey = bin2hex(random_bytes(16));
echo $apiKey; // e.g., "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
Step 2: Place the Key File
Create a text file at your domain root containing only your API key:
function createKeyFile(string $apiKey, string $webRoot): void {
$keyFilePath = rtrim($webRoot, '/') . '/' . $apiKey . '.txt';
file_put_contents($keyFilePath, $apiKey);
// Verify it's accessible
echo "Key file created at: {$keyFilePath}\n";
echo "Verify at: https://yoursite.com/{$apiKey}.txt\n";
}
This file proves to search engines that you own the domain.
Step 3: Build the Submission Client
Here's a complete, production-ready IndexNow client class:
class IndexNowClient {
private const ENDPOINT = 'https://api.indexnow.org/indexnow';
private const MAX_URLS_PER_BATCH = 10000;
private const TIMEOUT = 10;
public function __construct(
private readonly string $host,
private readonly string $apiKey,
private readonly string $keyLocation = '',
) {}
/**
* Submit a single URL
*/
public function submit(string $url): IndexNowResult {
return $this->submitBatch([$url]);
}
/**
* Submit multiple URLs in a single request
*/
public function submitBatch(array $urls): IndexNowResult {
if (empty($urls)) {
return new IndexNowResult(success: true, submitted: 0, message: 'No URLs to submit');
}
// Chunk if exceeding max batch size
if (count($urls) > self::MAX_URLS_PER_BATCH) {
return $this->submitChunked($urls);
}
$payload = [
'host' => $this->host,
'key' => $this->apiKey,
'urlList' => array_values($urls),
];
if ($this->keyLocation) {
$payload['keyLocation'] = $this->keyLocation;
}
$ch = curl_init(self::ENDPOINT);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json; charset=utf-8',
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::TIMEOUT,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return new IndexNowResult(
success: false,
submitted: 0,
message: "cURL error: {$error}"
);
}
return new IndexNowResult(
success: $httpCode >= 200 && $httpCode < 300,
submitted: count($urls),
message: "HTTP {$httpCode}",
httpCode: $httpCode
);
}
private function submitChunked(array $urls): IndexNowResult {
$chunks = array_chunk($urls, self::MAX_URLS_PER_BATCH);
$totalSubmitted = 0;
$errors = [];
foreach ($chunks as $chunk) {
$result = $this->submitBatch($chunk);
if ($result->success) {
$totalSubmitted += $result->submitted;
} else {
$errors[] = $result->message;
}
usleep(100000); // 100ms delay between batches
}
return new IndexNowResult(
success: empty($errors),
submitted: $totalSubmitted,
message: empty($errors) ? 'All batches submitted' : implode('; ', $errors)
);
}
}
readonly class IndexNowResult {
public function __construct(
public bool $success,
public int $submitted,
public string $message,
public int $httpCode = 0,
) {}
}
Step 4: Integrate with Your Content Pipeline
At DailyWatch, we call IndexNow at the end of each cron fetch cycle:
// After fetching and storing new videos
$newVideoIds = $db->getNewVideoIdsSinceLastFetch();
if (!empty($newVideoIds)) {
$urls = array_map(
fn(string $id) => "https://dailywatch.video/watch/{$id}",
$newVideoIds
);
$client = new IndexNowClient(
host: 'dailywatch.video',
apiKey: $config['indexnow_key']
);
$result = $client->submitBatch($urls);
echo "IndexNow: submitted {$result->submitted} URLs, "
. "status: {$result->message}\n";
}
HTTP Response Codes
| Code | Meaning |
|---|---|
| 200 | URLs submitted successfully |
| 202 | URLs accepted, key validation pending |
| 400 | Invalid request |
| 403 | Key not valid or not matching |
| 422 | URLs don't belong to the host |
| 429 | Too many requests (rate limited) |
Results
After implementing IndexNow on dailywatch.video, Bing indexing time dropped from 3-5 days to under 1 hour. The implementation took about 2 hours total, making it one of the highest-ROI SEO improvements available.
The full source code is available on GitHub. If you have questions about the implementation, drop them in the comments.
Top comments (0)