How to Connect the Google Indexing API to WordPress Without a Plugin
This guide is for WordPress site owners and developers who want new posts and updated pages submitted to the Google Indexing API automatically, without installing a third party plugin. You will learn what Google officially supports, how to create a service account and connect it to Search Console, and how to add a small block of PHP that sends URL_UPDATED and URL_DELETED notifications on publish, update, and trash. The focus keyword for this article is google indexing api wordpress, and every step uses plain PHP, WordPress hooks, and the official API endpoint. No extra plugin settings screens are needed, and you keep full control of keys, logs, and quotas.
Key takeaways
- The Google Indexing API officially supports JobPosting and BroadcastEvent pages only, so normal posts and pages are off label use with no guaranteed crawl.
- You can connect WordPress with 60 to 120 lines of PHP using transition_post_status, wp_remote_post, and a service account JSON key stored outside the web root.
- Store the JSON key outside public_html, restrict file permissions to 600, and cache the OAuth token in a transient to reduce auth calls.
- Queue submissions, limit to changed URLs only, and handle 403, 429, and JWT errors with logging and retry logic.
- Keep sitemaps, internal links, and Search Console verification in place, since API pings complement those signals and do not replace them.
- What connecting without a plugin actually means
- What Google officially supports and where WordPress fits
- Prerequisites before you touch any code
- Create a Google Cloud project and service account
- Add the service account to Search Console
- google indexing api wordpress: where to put the code
- Request an access token with JWT in PHP
- Submit URLs on publish update and delete
- Queue cron and quota safety
- Logging testing and status checks
- Maintenance rotation and troubleshooting
- FAQ
- Sources
- Further reading
<!-- IMAGE-PROMPT cover: 1200x630, DependsIt brand, deep charcoal #121212 or clean white background, vibrant mint #22E3B0 accent glow, thin node-network line art, Clash Display style bold heading space on left, General Sans clean labels, subject: WordPress dashboard linked to Google Indexing API notification flow, flat vector, high contrast, accessible, no photorealistic faces, no text smaller than 24px, no em dash in rendered text, export PNG then cwebp -q 82 to WEBP -->
What connecting without a plugin actually means
Connecting WordPress to the Google Indexing API without a plugin means adding your own PHP that talks directly to Google, instead of relying on a settings screen built by someone else. In practical terms, you create a service account in Google Cloud, give that account access to your Search Console property, then add a code block that fires when a post changes status. That code builds a short lived access token from your JSON key, then sends a POST request with the post URL and a notification type. The whole flow uses WordPress core functions such as wp_remote_post, get_permalink, set_transient, and error logging, so no additional tables or admin pages are required.
Many owners choose this path for three reasons. First, fewer plugins means fewer update conflicts and fewer vendor dependencies. Second, you control exactly which post types and which status changes trigger a submission, which helps you stay inside quota. Third, you control logging, so you can see response codes, timestamps, and message bodies in your own log file rather than in a black box interface. If you manage client sites, this control also makes audits simpler, because the code lives in version control and the key path is explicit.
This approach also gives you a clean wordpress indexing api path that stays in version control, plus a simple wordpress submit url google flow that only fires for public post types, and a modest wp indexing automation routine that logs each attempt. Teams that need wordpress seo indexing basics can keep sitemaps and internal links as the primary signals while the API handles fresh URLs.
There are trade offs to understand before you start. You take on maintenance for token refresh, error handling, and WordPress hook changes. You also need a safe place to store the JSON key, plus a plan for rotation if staff changes. A plugin would handle some of that for you, but it would also add its own assumptions about post types, retries, and logging locations. When you write the integration yourself, those decisions stay with you, which is useful if you run a small catalog of posts and pages and you want quiet, predictable behavior.
A no plugin setup still touches several systems, so plan the pieces in advance. You need a Google Cloud project with the Indexing API enabled, a service account with a JSON key, Search Console ownership for the exact domain or URL prefix, and a WordPress location for code such as a child theme functions.php file, a custom must use plugin, or a small site specific plugin. You also need a way to test, such as a staging post and a log viewer. If you manage multisite, decide whether the code runs network wide or per site, and decide which sites share the same service account. For background on quota behavior that affects this design, read about quota limits for the Indexing API before you set batch sizes. For the alternative of manual submission in Search Console, see the comparison of Indexing API versus Request Indexing covered in our sibling guide.
The rest of this guide walks through each piece in order. You will set up credentials, connect Search Console, place the code, handle tokens, submit on the correct hooks, add a queue for safety, and verify with logs. Each section includes copy paste starting points that you can adapt to your post types and your hosting layout. Keep your key private from the start, work on staging first when possible, and submit only URLs that changed, since repeated pings for unchanged URLs waste quota and add noise to logs.
| Approach | What you manage | Suitable when |
|---|---|---|
| Custom code in child theme | Hook, token, logs, key path | Single site, developer available |
| Must use plugin file | Same as above, survives theme switch | Client sites, theme changes often |
| Small custom plugin | Same, plus activation checks | Multisite or version controlled deploys |
| Third party plugin | Settings screen, vendor updates | No code access or no maintenance time |
What Google officially supports and where WordPress fits
Google documents the Indexing API for JobPosting pages and for BroadcastEvent pages inside VideoObject markup, typically livestreams. Those are the only types with official support. The endpoint accepts URL_UPDATED and URL_DELETED notifications, and Google may schedule a fresh crawl after receiving them. Normal blog posts, standard pages, product pages, and portfolio items are outside that documented scope. Many WordPress owners still submit those URL types, and the API often returns HTTP 200 with metadata, but a 200 response means Google received the notification, not that Google will crawl or index the URL. That distinction matters for expectations and for reporting to clients.
Google does not support IndexNow. That protocol is separate, maintained for Bing, Yandex, Naver, Seznam, and other participating engines, and it uses a plain text key file at the site root. Do not confuse the two systems. If you want Bing coverage from WordPress, use an IndexNow path in parallel, but keep credentials and logs separate. For Google, the path is OAuth 2.0 with a service account, Search Console verification, and the urlNotifications publish endpoint. The official prerequisites are described in the Indexing API prerequisites, which is the reference to trust when docs and blog posts disagree.
For WordPress, the practical fit depends on your content mix. A job board built on a custom post type named job, with valid JobPosting structured data, maps cleanly to official use. A news site that embeds livestreams with BroadcastEvent markup can also map cleanly for those video pages. A standard blog with how to posts and opinion pieces does not map to official use, so treat API pings as a crawl hint with uncertain effect, not as a ranking lever or a guarantee. Keep XML sitemaps updated, keep internal links from high traffic pages to new posts, keep canonical tags consistent, and keep server response times stable, since those factors influence crawl scheduling every day.
You should also set policy for which WordPress events trigger a ping. Recommended triggers are new publish, move from draft to publish, significant update to a published URL, and trash or delete for removal notices. Avoid triggering on autosave, revision save, preview, or bulk edits that do not change the canonical URL. Avoid resubmitting the full archive on theme activation or import, since that can exhaust quota in minutes. A short allowlist of post types, for example post and page, plus a check for public status and password protection, keeps behavior predictable.
| Content type | Official API support | WordPress handling |
|---|---|---|
| JobPosting with structured data | Yes | Submit on publish and on expiry or removal |
| BroadcastEvent livestream page | Yes | Submit on schedule change and on stream end |
| Standard post or page | Not documented | Optional off label ping, monitor logs, keep sitemaps |
| Product or archive page | Not documented | Prefer sitemap and internal links, ping sparingly if at all |
If you need background on policy and risk for normal pages, review the honest answer on normal pages. That context helps you explain the choice to site owners in plain terms and helps you decide whether to enable pings for all post types or only for structured types that match the docs.
Prerequisites before you touch any code
Before writing PHP, confirm four prerequisites: a Google account that can create Cloud projects, ownership of the Search Console property for the exact site, PHP with OpenSSL and JSON support on the host, and file system access to place code and store a key outside the web root. Most managed WordPress hosts already provide OpenSSL and JSON in PHP 7.4 and later, but verify with Site Health under Tools, then Site Health, then Info, then Server, or with a quick phpinfo check on staging. You also need the ability to set file permissions and to write to a private directory such as /home/user/private or /var/www/private, not to wp-content/uploads, which is web reachable.
Decide the exact property form in Search Console. A domain property covers all subdomains and protocols, while a URL prefix property covers one exact prefix such as https://example.com/. The service account must be added to the same property that contains the URLs you will submit. Mismatched properties are a common source of 403 errors. If you recently moved from http to https, or from www to non www, verify the canonical prefix and add the service account there. Keep a record of property type, since domain verification uses DNS while URL prefix can use HTML file, meta tag, or DNS.
Choose post types and events in advance. List the post types that should trigger pings, for example post and page, and exclude attachments, revisions, nav menus, and private types. Decide whether updates should retrigger. Many teams ping on first publish and on major updates only, where major means title, slug, canonical, or main content changed, not a typo fix. Decide how deletions are handled. When a post moves to trash, you can send URL_DELETED for the old permalink, but only if the URL now returns 404 or 410. If trash still renders a page or redirects, a delete notice may be premature, so check your trash behavior first.
Prepare logging and testing tools. Enable WP_DEBUG_LOG on staging, confirm wp-content/debug.log is writable and not publicly listed, and pick a separate log name for indexing events such as indexing-api.log in your private directory. Have a test post ready with a unique slug, and have the Search Console URL Inspection tool open for manual checks. Note your expected quota. New projects often start with a low daily publish quota, commonly reported around 200 requests per day for urlNotifications publish, with per minute limits that trigger 429 if you loop without delay. Plan to submit in small groups with pauses, and keep pacing as part of the design from day one since new projects often start with a low daily publish quota.
| Checklist item | Where to confirm | Done when |
|---|---|---|
| Cloud project access | Google Cloud Console IAM | You can create projects and keys |
| Search Console ownership | Search Console settings | You see Owner role for the property |
| PHP extensions | Site Health or php -m | openssl and json listed |
| Private key directory | SSH or file manager | Directory outside web root with 700 perms |
| Test post and log file | WordPress staging | You can publish and tail the log |
Create a Google Cloud project and service account
Create a dedicated Cloud project for indexing so usage, quotas, and key rotation stay isolated from other integrations. In the Cloud Console, create a new project named for the site, for example example-com-indexing, then open APIs and Services, then Library, search for Indexing API, and enable it for that project. Enabling the API is per project, so repeat for each project if you separate staging and production. After enabling, open IAM and Admin, then Service Accounts, and create a service account with a descriptive name such as wordpress-indexing-submitter. The service account email will look like wordpress-indexing-submitter@your-project.iam.gserviceaccount.com. Copy that email, since Search Console needs it in the next step.
Create a JSON key for the service account, then download it once and store it securely. In Service Accounts, open Keys, select Add Key, then Create New Key, then JSON. The browser downloads a file with private_key, client_email, token_uri, and related fields. Treat this file like a password. Do not commit it to public git, do not upload it to the media library, and do not paste it into support tickets or online tools. Move it to your private directory on the server, for example /home/user/private/indexing-service-account.json, and set permissions to 600 owned by the web user. Keep a backup in a password manager or secret store, then remove the download from your local Downloads folder if your policy requires it.
Restrict roles to the minimum needed. The service account calls the Indexing API with the indexing OAuth scope from your code, so it does not need broad Cloud roles. Avoid granting Owner or Editor at project level. If your organization requires a role for key creation auditing, use a short lived admin session to create the key, then return the service account to no extra roles. Document who can rotate keys and where the rotation runbook lives. If you use multiple environments, use separate service accounts for staging and production so a staging key leak does not affect the live property.
Record the key metadata you will need in PHP: client_email, private_key, and token_uri, typically https://oauth2.googleapis.com/token. Your code will use these to build a JSON Web Token, exchange it for an access token, and cache that token for about one hour. Do not hardcode the JSON content inside functions.php. Instead, load the file from the private path with file_get_contents and json_decode, so rotation means replacing one file. The setup steps for keys and roles are also covered in the official Indexing API prerequisites, which you should keep bookmarked for console label changes.
// Load service account JSON from outside the web root. No key material in code.
$key_path = '/home/user/private/indexing-service-account.json';
if (!file_exists($key_path)) {
error_log('[indexing] key file missing at ' . $key_path);
return;
}
$sa_json = file_get_contents($key_path);
$sa = json_decode($sa_json, true);
if (empty($sa['client_email']) || empty($sa['private_key'])) {
error_log('[indexing] service account JSON is incomplete');
return;
}
Verify the project and key before touching WordPress hooks. Use a server side test that requests an access token and prints the token expiry, without submitting any URL. If token exchange fails, fix it now, since every later step depends on auth. Common early failures include wrong private key formatting after copy paste, system clock drift on the server, and disabled Indexing API in the project. Set the server clock with NTP, keep the key file byte exact, and confirm the API shows as enabled in the Console dashboard.
Add the service account to Search Console
Search Console authorization is separate from Cloud IAM, and missing this step causes 403 permission denied on every publish call. Open Search Console, select the exact property that contains your WordPress URLs, then go to Settings, then Users and permissions, then Add user. Paste the full service account email, select Owner permission, and save. Owner is required for Indexing API publish calls on that property. Viewer or Full roles are not sufficient. If you use a domain property, add the service account at the domain property level. If you use URL prefix properties for http, https, www, and non www variants, add the account to the canonical variant that matches your WordPress home URL under Settings, then General.
After adding, verify the entry appears in the user list and note the date. Changes can take a few minutes to propagate, so wait before testing publish calls. If you manage clients, record who approved Owner access and when, since Owner can affect verification tokens and user management. For agencies, prefer one service account per client property rather than one shared account across all clients, because per client accounts limit blast radius and make quota tracking cleaner. If a client relationship ends, remove the service account from Search Console and delete the Cloud key.
Test authorization with a single harmless call before wiring hooks. The safest first test is getMetadata for a URL you own, which reads notification status without publishing. If getMetadata returns 403, the service account is not recognized as Owner on that property, or the property string mismatches. Check for trailing slash differences, www differences, and http versus https. If getMetadata returns 404 for a never submitted URL, auth is working and there is simply no notification history yet, which is expected. Only after auth passes should you test a publish call for a test post URL.
// Read notification status for a URL you own. Safe first auth test.
$endpoint = 'https://indexing.googleapis.com/v3/urlNotifications/metadata';
$args = array(
'method' => 'POST',
'headers' => array('Content-Type' => 'application/json'),
'body' => wp_json_encode(array('url' => 'https://example.com/test-post-for-auth/')),
'timeout' => 20,
);
Keep a short access table for the site. Document property type, canonical home URL, service account email, Cloud project ID, key creation date, and key holder. This table speeds up troubleshooting when 403 appears months later after a migration or domain mapping change. Migrations that change the canonical host often break the property match, so recheck Users and permissions after any move, CDN hostname change, or multisite domain mapping update. For background on permission failures and fixes, see how to fix 403 429 and JWT failures.
| Item | Example value | Where it lives |
|---|---|---|
| Property | https://example.com/ URL prefix | Search Console settings |
| Service account | wordpress-indexing@proj.iam.gserviceaccount.com | Cloud IAM plus Search Console users |
| Cloud project | example-com-indexing | Cloud Console dashboard |
| Key path | /home/user/private/indexing-service-account.json | Server private dir, 600 perms |
google indexing api wordpress: where to put the code
You have three stable locations for custom code: a child theme functions.php file, a must use plugin file under wp-content/mu-plugins, or a small site specific plugin under wp-content/plugins. A child theme is the quickest for a single site, but the code stops running if someone switches themes. A must use plugin always loads, cannot be disabled from the admin plugins screen, and survives theme changes, which makes it a strong choice for client work. A small custom plugin with a header comment behaves like a normal plugin, can be version controlled, and can be toggled for testing. Avoid putting keys or logic in the block editor custom HTML, in widgets, or in uploads, since those locations are fragile and often web readable.
Create a dedicated file so future developers can find it fast. For example, create wp-content/mu-plugins/dependsit-indexing.php with a header comment, a constant for the key path, and clearly named functions such as dependsite_get_access_token and dependsite_submit_url. Keep all indexing logic in that one file until it grows past a few hundred lines, then split token handling and queue handling into includes inside the same directory. Use a unique function prefix to avoid collisions with themes and other plugins. Add a guard with defined ABSPATH checks so the file cannot be invoked directly.
Set file permissions carefully. PHP files should typically be 644 and directories 755, while the JSON key stays at 600 in a private directory outside the web root. Never place the JSON key inside wp-content, inside the theme, or inside mu-plugins, since those paths can be served or listed by misconfigurations. Load the key by absolute server path, not by URL. If your host does not provide a private directory outside public_html, ask support for the recommended private path or use a managed secret store and environment variable. Document the path in your runbook so rotation does not require code search.
// wp-content/mu-plugins/dependsit-indexing.php
if (!defined('ABSPATH')) { exit; }
define('DEPENDSIT_INDEXING_KEY_PATH', '/home/user/private/indexing-service-account.json');
define('DEPENDSIT_INDEXING_LOG', '/home/user/private/indexing-api.log');
define('DEPENDSIT_INDEXING_POST_TYPES', array('post', 'page'));
Plan activation behavior. Code that sends API calls on plugin activation or theme switch can accidentally submit many URLs. Your file should define functions and add hooks, but should not loop over all posts on load. Imports, bulk edits, and WP CLI runs should also be excluded by checking DOING_CRON, WP_CLI, DOING_AUTOSAVE, and the current post status before submitting. Add a constant flag such as DEPENDSIT_INDEXING_ENABLED that you can set to false during migrations. This small amount of defensive code prevents quota burn during routine maintenance and keeps logs clean for real publishes.
<!-- IMAGE-PROMPT diagram-01: 1600px max, DependsIt brand mint #22E3B0 on charcoal #121212 or white, node-network line art, subject: WordPress publish hook to token cache to Indexing API flow diagram, flat vector, accessible, no em dash, Clash Display style headings, General Sans clean labels -->
Request an access token with JWT in PHP
The Indexing API uses OAuth 2.0 service account flow. Your PHP builds a JSON Web Token, signs it with the private_key from your JSON file, exchanges it at the token endpoint for a short lived access token, then uses that token as a Bearer header on publish calls. Tokens typically last 3600 seconds, so cache the token in a transient for about 55 minutes to avoid re signing on every post save. This design keeps post publish fast and reduces auth errors under load.
Build the JWT in three parts: header, claim set, and signature. Header specifies RS256 and JWT type. Claim set includes iss as client_email, scope as https://www.googleapis.com/auth/indexing, aud as token_uri, iat as current time, and exp as current time plus 3600. Sign the base64url encoded header and claim with openssl_sign using SHA256 and the private key, then base64url encode the signature and join the three parts with periods. Exchange the assertion at token_uri with grant_type urn:ietf:params:oauth:grant-type:jwt-bearer. Parse access_token from the JSON response and cache it. If openssl_sign fails, log the OpenSSL error string and stop, since retrying with the same key will fail the same way.
Handle time and encoding with care. Use time from the server clock, ensure NTP sync, and use base64url without padding, which means replacing plus with minus, slash with underscore, and trimming equals signs. Keep private_key newlines intact after json_decode, since reformatting breaks the signature. Set a 20 second timeout on the token request and treat non 200 responses as fatal for that run, with the error body written to your private log. Do not expose token errors in the admin notice HTML, since error bodies can include request metadata you do not want to leak to editors.
// Build and exchange JWT for an access token, then cache it for 55 minutes.
function dependsite_get_access_token() {
$cached = get_transient('dependsit_indexing_token');
if ($cached) { return $cached; }
$sa = json_decode(file_get_contents(DEPENDSIT_INDEXING_KEY_PATH), true);
$header = array('alg' => 'RS256', 'typ' => 'JWT');
$now = time();
$claims = array(
'iss' => $sa['client_email'],
'scope' => 'https://www.googleapis.com/auth/indexing',
'aud' => $sa['token_uri'],
'iat' => $now,
'exp' => $now + 3600,
);
$b64 = function($d) { return rtrim(strtr(base64_encode($d), '+/', '-_'), '='); };
$unsigned = $b64(wp_json_encode($header)) . '.' . $b64(wp_json_encode($claims));
$pkey = openssl_pkey_get_private($sa['private_key']);
if (!$pkey) { error_log('[indexing] bad private key'); return false; }
$sig = '';
if (!openssl_sign($unsigned, $sig, $pkey, OPENSSL_ALGO_SHA256)) {
error_log('[indexing] openssl_sign failed');
return false;
}
$jwt = $unsigned . '.' . $b64($sig);
$res = wp_remote_post($sa['token_uri'], array(
'timeout' => 20,
'body' => array(
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
),
));
if (is_wp_error($res)) { error_log('[indexing] token request failed: ' . $res->get_error_message()); return false; }
$data = json_decode(wp_remote_retrieve_body($res), true);
if (empty($data['access_token'])) { error_log('[indexing] no access token in response'); return false; }
set_transient('dependsit_indexing_token', $data['access_token'], 55 * MINUTE_IN_SECONDS);
return $data['access_token'];
}
Cache invalidation matters. Delete the transient when you rotate keys, when you see invalid_grant or invalid_token errors, and after staging restores that may carry stale transients. Add a small helper that clears the transient and forces a fresh exchange on the next run. Log token fetch duration so you can spot slow auth on shared hosting. If token calls regularly take more than a few seconds, move token refresh to cron rather than doing it inline during post save, so editors do not feel the delay.
Submit URLs on publish update and delete
Submit only on meaningful status transitions. Hook into transition_post_status, which passes new status, old status, and the post object, and filter to allowed post types, public visibility, and non password protected content. A common pattern is to submit URL_UPDATED when new status is publish and old status was not publish, or when an already published post is updated and a flag indicates a material change. Submit URL_DELETED when a published post moves to trash and its permalink will now 404, or when a published URL is permanently deleted. Always resolve the URL with get_permalink before the trash changes the slug, and store it for the delete call.
Guard against noise. Skip revisions, autosaves, nav menu items, attachments, and posts with no public permalink. Skip when DOING_AUTOSAVE, DOING_CRON bulk runs, or WP_CLI imports are active, unless you explicitly want CLI submissions. Check post_password, check that the post type is in your allowlist, and check that the URL starts with your canonical home URL to avoid submitting staging or preview domains. For updates, compare a hash of title plus content or check a custom checkbox such as Notify Google on major update, so typo fixes do not consume quota. These guards turn a noisy hook into a quiet pipeline that fires a few times per day instead of dozens.
Send the publish call with wp_remote_post to the urlNotifications publish endpoint path, using Bearer auth and a JSON body with url and type. Expect JSON with urlNotificationMetadata on success. Log the URL, type, HTTP code, and a timestamp on every call, and log the response body on non 200 codes. Do not loop synchronously over many URLs during post save. If an edit affects related URLs such as category archives, prefer updating the sitemap and internal links rather than pinging every related URL, since the API is per URL and quota is limited.
// Submit one URL with URL_UPDATED or URL_DELETED.
function dependsite_submit_url($url, $type) {
$token = dependsite_get_access_token();
if (!$token) { return false; }
$res = wp_remote_post('https://indexing.googleapis.com/v3/urlNotifications:publish', array(
'timeout' => 20,
'headers' => array(
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $token,
),
'body' => wp_json_encode(array('url' => $url, 'type' => $type)),
));
$code = is_wp_error($res) ? 0 : (int) wp_remote_retrieve_response_code($res);
$body = is_wp_error($res) ? $res->get_error_message() : wp_remote_retrieve_body($res);
error_log('[indexing] ' . $type . ' ' . $url . ' code=' . $code);
if ($code !== 200) { error_log('[indexing] body: ' . substr($body, 0, 2000)); }
return $code === 200;
}
add_action('transition_post_status', function($new, $old, $post) {
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; }
if (!in_array($post->post_type, DEPENDSIT_INDEXING_POST_TYPES, true)) { return; }
if (!empty($post->post_password)) { return; }
if ($new === 'publish' && $old !== 'publish') {
dependsite_submit_url(get_permalink($post->ID), 'URL_UPDATED');
} elseif ($new === 'trash' && $old === 'publish') {
dependsite_submit_url(get_permalink($post->ID), 'URL_DELETED');
}
}, 10, 3);
Test with a draft post that has a unique slug, publish it, then check your private log for code 200 and a metadata body. Then trash the test post and confirm a URL_DELETED entry appears. Use URL Inspection in Search Console to confirm crawl activity over the next hours, but treat timing as indicative, not promised. If you publish job posts with structured data, validate the JobPosting markup with the Rich Results Test flow and keep the validUntil or expiry logic consistent, since stale job pages that stay published after expiry create index bloat that API pings cannot fix.
Queue cron and quota safety
Inline submission during post save works for low volume sites, but it becomes fragile when editors bulk publish or when quota runs low. A queue decouples the WordPress save from the API call, so saves stay fast and submissions can be paced. The simplest reliable queue uses a custom option or a custom table that stores pending URL plus type plus attempts plus next retry timestamp. On transition_post_status, push to the queue instead of calling the API directly. A cron job every five to ten minutes pops one to five items, sends them with a short sleep between calls, and updates status to sent, retry, or dead letter after three failures.
For a wordpress index new posts flow, use a five minute wp cron indexing schedule that pops a few items per run, which keeps wordpress fast indexing steady without bursts. The same queue can handle functions.php indexing snippets moved to mu-plugins later, since the storage format stays the same.
Respect quota and rate limits explicitly. Treat daily publish quota as a budget, not a target, and track usage in the same table or in a daily counter option. Pause the worker when usage reaches 80 percent of your known quota, and resume after the quota reset window. On HTTP 429, read any Retry After hint, apply exponential backoff such as 60 seconds, then 300 seconds, then 900 seconds, and jitter the delay so concurrent workers do not retry in lockstep. On 403, do not retry blindly, since permission errors rarely clear without a config fix. On 5xx, retry with backoff and keep the item. Log request IDs when present, since they help correlate with Cloud Console metrics.
WP Cron versus server cron matters on low traffic sites. WP Cron only runs when the site receives visits, so a queue can stall overnight. If timely submission matters, disable WP Cron handling delays by adding a real system cron that hits wp-cron.php every five minutes, or move the worker to a server cron that calls WP CLI. Keep the worker idempotent, so reprocessing the same queue row does not create duplicates. Mark rows as claimed with a worker timestamp before sending, so two overlapping runs do not send the same URL twice.
// Push to queue on publish instead of sending inline. Worker sends later.
function dependsite_queue_url($url, $type) {
$q = get_option('dependsit_indexing_queue', array());
$q[] = array('url' => $url, 'type' => $type, 'tries' => 0, 'next' => time());
update_option('dependsit_indexing_queue', array_slice($q, -500), false);
}
Size the worker to your reality. A blog publishing three posts per week needs a worker that sends one URL per run with no complexity. A job board adding 100 posts per day needs batching, daily caps, priority for new jobs over minor updates, and dead letter review. Never advise hammering the endpoint to catch up after an import. Instead, prioritize fresh and high value URLs, update the sitemap lastmod timestamps, strengthen internal links from the homepage and category pages, and spread the backlog over days using the safe bulk submission patterns described in our sibling guide.
<!-- IMAGE-PROMPT workflow-02: 1600px max, DependsIt brand, subject: WordPress queue and cron workflow to Indexing API with backoff, flat vector, accessible, no em dash, mint #22E3B0 on charcoal #121212, node-network line art, Clash Display style headings, General Sans clean labels -->
Logging testing and status checks
Good logs turn API integration from guesswork into routine checks. Log every submission attempt with timestamp, URL, notification type, HTTP status code, tries count, and a short response excerpt. Keep a separate error log for auth failures, 403 permission issues, and 429 throttles, so patterns stand out. Store logs outside the web root, rotate them weekly, and redact the Bearer token if you ever log headers. In WordPress, use error_log to your private file or use a small logger that writes JSON lines, which makes grep and basic analysis simple. Avoid storing full response bodies for 200 successes beyond the first week, since they add volume without diagnostic value.
Test in layers. First, test token exchange in isolation and confirm a cached transient appears. Second, test getMetadata for a known URL to confirm Search Console permission. Third, publish a test post with a unique slug and confirm a 200 with urlNotificationMetadata containing the same URL and a recent notifyTime. Fourth, trash that test post and confirm URL_DELETED handling. Fifth, simulate failures by temporarily pointing the key path to a missing file and confirming the code logs clearly instead of fataling the post save. Each layer isolates auth, permission, hook wiring, and error handling, so a failure points to one area.
Use getMetadata to check notification history without resubmitting. Send the URL in a JSON body to the metadata endpoint path and inspect latestUpdate and latestRemove fields when present. A recent notifyTime after your publish confirms receipt. Absence of history for a never submitted URL is normal. Do not poll getMetadata in a tight loop, since that also consumes resources and adds noise. Check once after publish, then rely on Search Console coverage reports and server logs for crawl evidence. Keep a field guide for Indexing API status codes nearby when reading logs.
// cURL check for notification metadata. Run from a server shell, not from WordPress.
// Replace TOKEN and URL with your values. Inspect latestUpdate and notifyTime.
// curl -s -H "Content-Type: application/json" -H "Authorization: Bearer TOKEN"
// -d '{"url":"https://example.com/test-post-for-auth/"}'
// "https://indexing.googleapis.com/v3/urlNotifications/metadata"
Add a lightweight admin indicator without building a full settings page. A dashboard widget or a Tools page that shows queue length, last ten log lines, token expiry, and daily usage gives editors and developers a shared view. Keep the page capability restricted to manage_options, escape all output, and never display the private key or full token. If you prefer WP CLI, add a command such as wp indexing status that prints queue depth and last error, which fits version controlled workflows and avoids extra admin UI.
| Test | Expected result | If it fails |
|---|---|---|
| Token exchange | access_token cached 55 min | Check key path, clock, API enabled |
| getMetadata for owned URL | 200 or 404 with no auth error | Fix Search Console Owner entry |
| Publish test post | 200 with notifyTime | Check scope, property match, logs |
| Trash test post | URL_DELETED logged 200 | Confirm permalink capture before trash |
| Missing key simulation | Logged error, post still saves | Wrap API calls in safe guards |
Maintenance rotation and troubleshooting
Treat the integration as a small production service with a runbook. Review logs weekly for the first month, then monthly. Track counts of 200, 403, 404, 429, and 5xx by week, plus queue depth and oldest pending item age. Set an alert when queue depth exceeds 50 or when oldest pending age exceeds 24 hours, since those signs point to quota exhaustion, auth failure, or a stuck worker. After theme changes, host migrations, or domain mapping updates, retest token exchange and one publish, and reconfirm the service account still holds Owner on the canonical property.
Rotate keys on a schedule and on staff changes. To rotate without losing submissions, create a second JSON key for the same service account, deploy it to the private path under a new filename, update the constant to point to the new file, clear the token transient so the next run uses the new key, then monitor for 200s before deleting the old key. Keep both keys valid for 24 to 48 hours during the switch, then delete the old key in Cloud Console and note the rotation date. Never email keys, never store them in tickets, and never leave old keys enabled indefinitely. Document rotation in the same access table created during setup.
Troubleshoot with a fixed order: auth, permission, request, quota, content. Auth issues show as failed to parse JWT, invalid grant, or invalid signature, often caused by clock drift, truncated private_key, or wrong scope. Permission issues show as 403 with permission denied, caused by missing Search Console Owner or property mismatch. Request issues show as 400 with invalid URL or invalid type, caused by preview URLs, relative URLs, or typos in URL_UPDATED. Quota issues show as 429 with quota exceeded, which calls for backoff and pacing, not retries at full speed. Content issues appear later as crawled but not indexed, which points to thin content, duplicate canonicals, or noindex, not to API delivery. Use the standard error playbook for 403, 429, and JWT failures to map each code to a next step.
// Safe rotation helper. Point constant to new file, then clear cached token.
// delete_transient('dependsit_indexing_token');
// Tail the private log for 200s before deleting the old key in Cloud Console.
Keep WordPress hygiene alongside the integration. Update sitemaps automatically on publish, keep robots.txt from blocking new paths, keep canonical tags self referencing on canonical URLs, and keep internal links pointing to fresh posts from relevant hubs. If coverage reports show discovered but not indexed for many new posts, the bottleneck is crawl prioritization or content evaluation, not notification delivery, so improve internal linking and content depth before increasing ping volume. For that diagnosis, see official Search Console help for coverage states, plus the practical walkthrough for request indexing alternatives in our sibling guide.
FAQ
Does this code work for normal blog posts and pages?
It can send notifications for any public URL you own, and Google often returns 200, but official support covers JobPosting and BroadcastEvent pages only. Treat pings for normal posts as hints with no promised crawl. Keep sitemaps and internal links current, and monitor coverage to see actual effect.
Do I need a plugin if I use this code?
No. The code in this guide replaces a plugin for submission, token handling, and basic queueing. You still need a Google Cloud project, a service account key, and Search Console access. If you prefer a dashboard UI or automatic retries with reporting, a maintained tool may suit you better than custom code. If you compare this route with an indexing api plugin wordpress option, the trade off is control versus convenience, and both still need an auto submit wordpress rule that only fires for changed public URLs.
Where should I store the JSON key on shared hosting?
Store it outside public_html in a private directory with 600 permissions, and load it by absolute path. Do not place it in wp-content, uploads, or the theme folder. If your host offers no private directory, ask support for the recommended path or use environment based secrets.
What happens if I hit quota during bulk publishing?
The API returns 429 and your worker should pause, back off, and retry later. Queue new URLs, cap daily sends, prioritize new and high value pages, and spread backlog over days. Avoid tight retry loops, since they extend the throttle window.
How do I confirm Google received a notification?
Check your log for HTTP 200 with urlNotificationMetadata and a recent notifyTime, then call getMetadata once for the same URL. A 200 confirms receipt, not indexing. Use Search Console coverage and URL Inspection over the following days to observe crawl and index state.
Should I also use IndexNow from WordPress?
IndexNow targets Bing, Yandex, and other participating engines, while Google does not support it. If Bing traffic matters to you, add IndexNow alongside this Google path with separate keys and logs. Keep sitemaps in place for both ecosystems, since pings complement sitemaps and do not replace them.
Sources
- https://developers.google.com/search/apis/indexing-api/v3/prereqs
- https://developers.google.com/search/apis/indexing-api/v3/quickstart
- https://support.google.com/webmasters/answer/7440203
- https://schema.org/JobPosting