About 10 minutes: one Google Cloud service account for the Google Indexing API, one key file for IndexNow (Bing, Yandex, Seznam, Naver), three environment variables. Each engine works on its own: you can start with IndexNow only and add Google later.
| Step | Where | Needed for |
|---|---|---|
| 1. Upload the script | Your PHP host | Both |
| 2. Project + Indexing API | Google Cloud Console | |
| 3. Service account + JSON key | Google Cloud Console | |
| 4. Owner in Search Console | Google Search Console | |
| 5. IndexNow key file | Your site root | IndexNow |
| 6. Environment variables | Your PHP host | Both |
| 7. Daily queue cron | Your server crontab | |
| 8. Check the result | The script page | Both |
| 9. Job boards: sitemap, expired jobs, WordPress, API | Your site / CMS |
Requirements: PHP 8.0 or newer with the curl, openssl and
zlib extensions (almost all hosts have them). On cPanel, check "Select PHP Version" (or
"MultiPHP Manager") first: PHP 7.4 or older will not run the script. To check from a shell:
php -v and php -m | grep -E 'curl|openssl|zlib'.
Put index.php in its own folder on that host, behind Apache or Nginx+php-fpm. The folder must
be writable by PHP: the script keeps its log (gi-log.php) and its queue
(gi-queue.php) next to itself, both closed to web visitors. To try it on your computer first:
php -S 0.0.0.0:8080 # then open http://localhost:8080
With nothing configured it runs in demo mode: same flow, no call leaves, every row says so.
indexer@your-project.iam.gserviceaccount.com: you need it in step 4..json file downloads./home/you/indexer-key.json): it is a credential, nobody should be able to download it.If key creation is blocked with "Service account key creation is disabled", your
organisation enforces the policy iam.disableServiceAccountKeyCreation: an admin can lift it
for this project in IAM & Admin > Organization Policies, or create the project
outside the organisation.
This is the step people miss. Google accepts URLs only for properties the service account owns: with valid credentials but without this step every Google row answers 403 Permission denied. Failed to verify the URL ownership.
https:// and www form you submit).Full permission added from Add user is not enough: it must be Owner. Several sites? Repeat this step on each property with the same service account.
IndexNow works for every page of your site and needs no Google account. Pick a key of 8-128 letters, digits or dashes, for example:
php -r 'echo bin2hex(random_bytes(16)), "\n";'
Publish it at the root of each site you submit, as a text file named after the key and containing only the key:
https://your-site.com/<key>.txt → <key>
Open that address in a browser: if you see the key, the engines will too.
| Variable | Value |
|---|---|
GOOGLE_SERVICE_ACCOUNT_JSON | Absolute path to the JSON key from step 3. |
GOOGLE_SERVICE_ACCOUNT_JSON_INLINE | Alternative: the JSON content itself, for hosts where you cannot place files (use one of the two). |
GOOGLE_SERVICE_ACCOUNT_DIR | Alternative for more than 200 URLs/day: a folder holding several JSON keys (service accounts from different Google Cloud projects, each Owner in Search Console). When one spends its daily quota the next takes over; the page shows what each has left. Keep this folder, like any key, outside the web root. |
INDEXNOW_KEY | The key from step 5. |
GI_PASSWORD | A password for the page: the browser asks for it (any username). Without it, anyone who finds the page can spend your quota and read your log, and the page shows a warning. |
GI_API_TOKEN | Optional: a long random token that lets your CMS or the WordPress bridge send URLs to the JSON API (step 9). Without it the API is off. |
INDEXER_DAILY_QUOTA | Optional: only if Google raised your quota above the default 200 URLs/day. |
Apache (.htaccess or the virtual host):
SetEnv GOOGLE_SERVICE_ACCOUNT_JSON /home/you/indexer-key.json SetEnv INDEXNOW_KEY your-key SetEnv GI_PASSWORD choose-a-long-password
Nginx + php-fpm (in the location ~ \.php$ block):
fastcgi_param GOOGLE_SERVICE_ACCOUNT_JSON /home/you/indexer-key.json; fastcgi_param INDEXNOW_KEY your-key; fastcgi_param GI_PASSWORD choose-a-long-password;
Shared hosting or cPanel with no way to set variables: create config.php next to
index.php with the same names; an environment variable, when set, wins over the file.
<?php
return [
'GOOGLE_SERVICE_ACCOUNT_JSON' => '/home/you/private/indexer-key.json',
'INDEXNOW_KEY' => 'your-key',
'GI_PASSWORD' => 'choose-a-long-password',
'GI_API_TOKEN' => 'a-long-random-token',
];
Local try:
GOOGLE_SERVICE_ACCOUNT_JSON=/home/you/indexer-key.json INDEXNOW_KEY=your-key php -S 0.0.0.0:8080
Google's default quota is 200 URLs/day per project. URLs beyond today's quota, or refused with
429, go to the queue (gi-queue.php), and the page tells you how many are waiting. One cron line sends them
every night; cron does not see the web server's variables, so repeat the Google one there:
0 3 * * * GOOGLE_SERVICE_ACCOUNT_JSON=/home/you/indexer-key.json php /path/to/index.php --process-queue
With config.php the cron needs no variables: php /path/to/index.php --process-queue.
URLs that hit a temporary Google error (5xx, timeout) are queued too and retried by the same cron.
Run it as the same user as PHP (e.g. crontab -u www-data -e) so it can write
the queue and the log. IndexNow has no queue: up to 10,000 URLs go in one call, at once.
Open the page, paste two or three URLs of your site and submit. Every URL gets one row per engine:
| Answer | Meaning |
|---|---|
| Google 200 | Accepted. Google guarantees faster crawling only for pages with JobPosting or BroadcastEvent structured data; other pages are accepted with no promise. |
| Google 403 | The service account is not Owner of that property: back to step 4, and check the property matches the URL form (www, https). |
| Google 429 | Quota used up: the URL is queued and the cron sends it tomorrow. |
| IndexNow 200 / 202 | Accepted (202: the key is still being checked). |
| IndexNow 403 | The key file is missing or different: open https://your-site.com/<key>.txt. |
| off | That engine is not configured: its variable is missing or not visible to PHP. |
Whole sitemap. Paste the sitemap address in the page (sitemap index and .xml.gz work),
or let cron send it every night; add --deleted for a sitemap of removed jobs:
30 2 * * * php /path/to/index.php --sitemap https://your-board.com/job-sitemap.xml 45 2 * * * php /path/to/index.php --urls /path/to/expired-urls.txt --deleted
Expired jobs. Google for Jobs keeps showing a posting until it is told the page is gone.
Choose "Removed" in the page, or send the URL with type deleted: it goes out as URL_DELETED.
WordPress (WP Job Manager, WP Job Openings). Download the free
bridge, save it as wp-content/mu-plugins/google-indexer-wp.php
and add two lines to wp-config.php:
define('GI_ENDPOINT', 'https://your-board.com/indexer/index.php');
define('GI_API_TOKEN', 'the same token as GI_API_TOKEN above');
A published job is sent as URL_UPDATED; an expired, unpublished or deleted job as URL_DELETED.
Publishing never waits for Google.
Any other CMS. One HTTP call when a job goes live or comes down:
curl -X POST 'https://your-board.com/indexer/index.php?api=1' \
-H 'X-GI-Token: a-long-random-token' -H 'Content-Type: application/json' \
-d '{"urls":["https://your-board.com/jobs/123"],"type":"updated"}'
The answer is JSON: {"ok":true,"results":[{"url":"…","status":"accepted"}],"queued":0};
"type":"deleted" removes the URL. A wrong or missing token gets 401.
From now on: paste the new URLs, submit, read each engine's answer; the log keeps every submission.
Google Instant Indexer — $24.99 one-time
Stuck on a step? Write to deus@lumnika.com with the answer the page shows for that URL.