Indexer by DependsiT

Google Indexing API with PHP: Submit and Check URL Status

Google indexing api php submit flow with JWT and cURL

This guide explains google indexing api php integration for submitting URLs and checking notification status with plain PHP and cURL. It is for PHP developers, WordPress implementers, and site owners who run shared hosting, VPS, or dedicated servers and want copy paste code that works without heavy frameworks. You will build JWTs, exchange them for access tokens, POST URL_UPDATED and URL_DELETED notifications, read metadata, handle quota errors, and hook submissions into WordPress safely.

Key takeaways

  • PHP can call the Indexing API with only OpenSSL, JSON, and cURL, plus your service account JSON key, no SDK required.
  • The API is documented for JobPosting and BroadcastEvent pages, so track Search Console results carefully for other page types.
  • Build JWTs with correct base64url encoding, one hour expiry, and exact indexing scope, then cache tokens until near expiry.
  • Queue bulk work with pacing and backoff, log every attempt, and keep daily volume below your Cloud quota.

Google indexing api php submit flow with JWT and cURL <!-- 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: PHP code with cURL submitting URLs to Google Indexing API and JSON response, 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 -->

How PHP submission works without a SDK

PHP can call the Indexing API with core extensions only, which makes it practical on shared hosting where you cannot install Composer packages or system libraries. The flow has five steps that repeat per batch. Load the service account JSON from a secure path outside the web root. Build a JWT header and claim set, sign it with the RSA private key using OpenSSL, and exchange it for an access token at the Google OAuth token endpoint. Cache that token in memory or in a temp file until near expiry. POST JSON with url and type to the publish endpoint using cURL. Log HTTP status, notifyTime, and error bodies, then optionally GET metadata to confirm stored state. Each step uses plain PHP functions you can inspect and test in isolation.

The HTTP contract is identical across languages. POST to indexing.googleapis.com/v3/urlNotifications:publish with URL_UPDATED for new or changed pages and URL_DELETED for removed pages. GET from indexing.googleapis.com/v3/urlNotifications/metadata with a url parameter to read the last notification. The API returns urlNotificationMetadata with notifyTime on success. It does not return crawl or index state. Those signals come later from Search Console coverage, URL Inspection, and server logs showing Googlebot visits. Keeping this separation clear helps you set stakeholder expectations and design logs that show both submission health and downstream movement.

PHP developers sometimes expect an official SDK method for every endpoint. For this API, direct HTTPS gives you the clearest control over timeouts, retries, and logging, and it avoids version drift in third party wrappers. You still use Google libraries indirectly through the token endpoint, but the Indexing API calls themselves are simple JSON over HTTPS. That simplicity is an advantage on constrained hosts. A single file with functions for base64url encoding, JWT creation, token exchange, publish, and metadata can power a whole site, with a queue file or database table handling bulk work.

Scope clarity matters before you automate. Google documents the Indexing API for JobPosting pages and BroadcastEvent pages tied to livestreams, with structured data requirements in its search guides. If your PHP site publishes jobs or live events, you are inside documented use. If you submit posts, products, or category pages, treat each 200 as an accepted hint with varied outcomes, keep sitemaps fresh and internal links clean, and verify with Search Console rather than assuming instant indexing. Setup of the underlying identity is covered in the service account setup guide, with broader context in the complete setup guide for faster indexing.

Budget about one hour from empty folder to first 200 on a typical LAMP host if Search Console delegation already exists. Most of that time goes to key placement, JWT debugging, and cURL timeout settings. Keep first tests to one canonical URL you control, with 200 status, indexable robots, self referencing canonical, and valid structured data where applicable. Log every step from the start, even manual tests, so later WordPress hooks inherit observability instead of silent behavior.

Prerequisites and key setup for PHP hosts

You need a Cloud project with the Indexing API enabled, a service account JSON key, and Owner delegation for that service account email on each Search Console property you will submit for. Confirm in the consoles before writing PHP. Check Enabled APIs for Indexing API. Check IAM Service Accounts for your account and key ID. Check Search Console Users and permissions for Owner status on each domain or prefix property. Record project ID, service account email, property list, quota values, and key creation date in a runbook. PHP cannot fix missing delegation with code. A 403 permission denied always requires console action.

Your PHP environment needs version 8.0 or newer, with ext-openssl, ext-json, and ext-curl enabled. Verify with php -m and php -v on the same host and user that will run submissions. Shared hosts sometimes disable curl exec functions or outbound HTTPS to token endpoints. Test with a simple cURL GET to a public Google endpoint and with an OpenSSL sign test locally. Confirm system time with NTP, because JWTs carry issued at and expiry and skew beyond a few minutes causes invalid grant errors that look like key corruption. Also confirm allow_url_fopen or cURL availability for token exchange, depending on which HTTP path your code uses.

Prepare test URLs that separate code correctness from content quality. Use one job posting or livestream page for documented scope, plus one stable article you own for general behavior. Each should return 200, allow crawling, have no noindex, and use absolute canonicals matching the submitted string exactly. Avoid staging domains, query session IDs, and faceted filters for first runs. Note URL, property, and structured data type for each test. This makes later log review direct when you compare publish time, notifyTime, metadata state, and Search Console coverage.

Plan key storage for PHP hosting realities. On VPS or dedicated servers, place the JSON outside the web root, for example /etc/secrets/indexing-publisher-key.json with owner www-data or your deploy user and mode 600. On shared hosting without access above web root, place it in a password protected directory with deny rules, or better, store the JSON content in a host secret or env variable and write it to a temp file at runtime with mode 600. Never commit the JSON to git, never upload it through the media library, and never paste it into support chats or docs. If a vendor needs to submit, have them use their own Cloud project and grant their service account Owner on your property, rather than sending your key.

Project layout and dependencies for PHP

Keep the PHP integration small and explicit. One folder with a config file, a library file for JWT and HTTP helpers, a submit script, a status script, a queue file or table, and a logs folder is enough for most sites. WordPress sites can mirror the same files inside a must use plugin or a custom plugin folder, with hooks calling the library. The layout below suits both plain PHP and WordPress without a public plugin dependency.

mkdir -p /opt/indexer-php/logs /opt/indexer-php/urls
php -v
php -m | grep -E "openssl|curl|json"
ls -l /etc/secrets/indexing-publisher-key.json

No Composer packages are required for the core flow, which makes a lean indexing api php script practical on shared hosting. OpenSSL signs the JWT, hash_hmac is not needed for RS256, JSON functions encode payloads, and cURL posts to token and publish endpoints. Teams that prefer helpers can use the php google client library or google api php client wrappers for JWT building, but each dependency adds update work. An indexing api curl php approach with core cURL keeps timeouts and retries visible. Start with core only, add libraries only when you need features like automated token refresh across many workers. Pin PHP minor version in your host docs so upgrades are deliberate.

Configuration belongs in environment variables or a config file outside the web root, not hardcoded in scripts. Required values are key path, property root, daily quota target, queue path, and log path. The example below shows a config file that both CLI scripts and WordPress hooks can include. Adjust paths for your host and keep the URL list as a text file with one URL per line for early testing. Later you can source URLs from WP_Query, a sitemap parser, or a database query, but a file keeps first runs auditable.

<?php
return [
  'key_path' => getenv('INDEXING_KEY_PATH') ?: '/etc/secrets/indexing-publisher-key.json',
  'property' => 'https://example.com/jobs/',
  'quota_per_day' => 150,
  'queue_path' => '/opt/indexer-php/urls/queue.jsonl',
  'log_path' => '/opt/indexer-php/logs/indexer.log',
];

Verify the layout with a checklist before coding JWTs. Confirm php -v shows 8.0 or newer. Confirm openssl, curl, and json appear in php -m. Confirm the key file exists at the configured path with mode 600 for the runtime user, tested with sudo -u www-data cat for web workers and with your deploy user for CLI. Confirm outbound HTTPS to oauth2.googleapis.com and indexing.googleapis.com with a short cURL test. If any check fails, fix hosting or permissions now, because later API errors will be harder to separate from environment blocks.

Building and signing JWTs in PHP

JWT construction in PHP has three parts. Base64url encode a header with alg RS256 and typ JWT. Base64url encode a claim set with iss as client_email, scope as the indexing scope, aud as token URI, iat as now, and exp as now plus 3600. Sign the header dot payload string with the RSA private key using SHA256, base64url encode the signature, and join the three segments. Exchange that assertion for an access token at the token URI with grant_type urn:ietf:params:oauth:grant-type:jwt-bearer. Cache the token until about five minutes before expiry and reuse it across requests in the batch.

Base64url encoding differs from standard base64 in three characters and padding. Use the helper below consistently for header, payload, and signature. Standard base64_encode followed by character replacement and padding trim is the reliable pattern. Inconsistent encoding is a common cause of invalid signature errors that look like key problems but are actually formatting bugs. Centralize encoding in one function used by every JWT path, including WordPress hooks and CLI scripts, so fixes apply everywhere at once.

<?php
function base64url_encode(string $data): string {
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function build_jwt_assertion(array $key, int $now): string {
  $header = base64url_encode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
  $payload = base64url_encode(json_encode([
    'iss' => $key['client_email'],
    'scope' => 'https://www.googleapis.com/auth/indexing',
    'aud' => $key['token_uri'],
    'iat' => $now,
    'exp' => $now + 3600,
  ]));
  $input = $header . '.' . $payload;
  $pkey = openssl_pkey_get_private($key['private_key']);
  openssl_sign($input, $sig, $pkey, OPENSSL_ALGO_SHA256);
  return $input . '.' . base64url_encode($sig);
}

Exchange the assertion for a token with a POST to token_uri. The snippet below loads the JSON key, builds the assertion, posts the grant, and returns access token plus expiry. It uses cURL with explicit timeouts and error capture so hosting blocks show up clearly. Run it once in isolation before publish logic, and print only token prefix and expiry, never the full token or private key. If it returns an access token, your key, clock, scope, and encoding are correct.

<?php
function fetch_access_token(array $cfg): array {
  $key = json_decode(file_get_contents($cfg['key_path']), true);
  $now = time();
  $assertion = build_jwt_assertion($key, $now);
  $ch = curl_init($key['token_uri']);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_POSTFIELDS => http_build_query([
      'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      'assertion' => $assertion,
    ]),
  ]);
  $body = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  $err = curl_error($ch);
  curl_close($ch);
  if ($body === false) { throw new RuntimeException('token HTTP failed: ' . $err); }
  $data = json_decode($body, true);
  if ($code !== 200 || empty($data['access_token'])) { throw new RuntimeException('token error ' . $code . ': ' . substr($body, 0, 500)); }
  return [$data['access_token'], $now + (int)($data['expires_in'] ?? 3600)];
}

If token exchange fails with invalid grant or invalid signature, check clock sync, re-download the JSON to rule out corruption, confirm client_email matches Search Console exactly, and verify scope string has no trailing spaces. Most php jwt google failures trace to base64url mistakes, edited private key newlines, or clock skew rather than IAM roles. Do not add IAM roles to fix signing. Roles do not repair signatures. Fix key, time, and encoding first, then retest this function alone before resuming publishes. Log key ID and service account email at startup to simplify later 403 debugging.

Submitting URL_UPDATED with google indexing api php and cURL

Publishing from PHP is a cURL POST with Bearer auth and JSON body. Reuse the cached access token across URLs in the batch, refreshing when less than five minutes remains. Set Content-Type to application/json, Authorization to Bearer plus token, and timeout to 30 seconds. Send url as absolute string matching your Search Console property and type as URL_UPDATED for new or changed pages. Log HTTP code, response body prefix, and notifyTime on success. The function below shows the shape with explicit error capture for hosting diagnostics.

<?php
function publish_url(string $token, string $url, string $type = 'URL_UPDATED'): array {
  $ch = curl_init('https://indexing.googleapis.com/v3/urlNotifications:publish');
  $payload = json_encode(['url' => $url, 'type' => $type]);
  curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token],
    CURLOPT_POSTFIELDS => $payload,
  ]);
  $body = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  $err = curl_error($ch);
  curl_close($ch);
  if ($body === false) { return ['http' => 0, 'error' => $err]; }
  return ['http' => $code, 'body' => $body];
}

list($token, $exp) = fetch_access_token($cfg);
$res = publish_url($token, 'https://example.com/jobs/senior-support-specialist', 'URL_UPDATED');
echo 'HTTP ' . $res['http'] . PHP_EOL;
echo substr($res['body'] ?? $res['error'] ?? '', 0, 1000) . PHP_EOL;

Validate URLs before POST to protect quota. To php submit url to google safely, keep a small php index script that trims whitespace, requires absolute https, enforces your canonical slash policy, and preflights robots and meta robots. Each php url notification should use the identical normalized string for publish and metadata, so logs join cleanly. Submitting noindex or disallowed URLs returns 200 at the API layer but leads nowhere in search. For job sites, submit on posting creation and on material updates such as title, location, or closing date. For livestreams, submit on schedule publication and on going live. For general pages used as a hint, submit on creation and on substantial updates, not on every typo fix. Centralize normalization so publish and metadata use identical strings.

Success is 200 with urlNotificationMetadata containing url, latestUpdate, and notifyTime. Save full response with timestamp and request context. A 200 means the hint was recorded, not that the page is indexed. Follow with Search Console URL Inspection and server log checks for Googlebot in the coming days. For language parity in mixed teams, compare retry and logging patterns with our Python tutorial for submitting URLs and checking status.

Google indexing api php publish flow from JWT signing to URL_UPDATED <!-- IMAGE-PROMPT diagram-01: 1600px max, DependsIt brand mint #22E3B0 on charcoal #121212 or white, node-network line art, subject: PHP publish flow diagram showing JSON key then JWT build then token exchange then POST publish, flat vector, accessible, no em dash, Clash Display style headings, General Sans labels -->

Checking status with getMetadata in PHP

Metadata reads confirm what Google last stored for a URL through this channel. Use GET on urlNotifications/metadata with the URL encoded as a query parameter and the same Bearer token. A stored notification returns 200 with latestUpdate and notifyTime. A never submitted URL returns 404 with a clear message. Use this to audit queue output, confirm worker behavior, and separate never submitted from submitted but not yet crawled. It is not an index status check. Use URL Inspection for index state.

<?php
function get_metadata(string $token, string $url): array {
  $endpoint = 'https://indexing.googleapis.com/v3/urlNotifications/metadata?url=' . rawurlencode($url);
  $ch = curl_init($endpoint);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
  ]);
  $body = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  curl_close($ch);
  return ['http' => $code, 'body' => $body];
}

$meta = get_metadata($token, 'https://example.com/jobs/senior-support-specialist');
echo 'Metadata HTTP ' . $meta['http'] . PHP_EOL;
echo substr($meta['body'], 0, 1000) . PHP_EOL;

Schedule metadata checks separately from publishes. A practical rhythm is one check per URL the next day, during a reporting job, plus immediate checks for failures needing diagnosis. For bulk audits, sample ten percent plus all errors instead of reading every URL hourly. Metadata uses a separate read quota that is usually generous, but noisy polling hides publish quota pressure in shared dashboards. Store metadata results alongside publish attempts with publish time, publish status, metadata time, and metadata type for clean joins.

Watch for normalization mismatches that mimic API bugs. Publish with https://example.com/jobs/role and metadata lookup with https://example.com/jobs/role/ are different keys. One can return 200 while the other returns 404, even though both strings look similar to humans. Use one normalize function for both paths, handling protocol, host case, trailing slashes, and query stripping per your canonical policy. Store the normalized string in queue and logs so joins work without manual cleanup.

Sending URL_DELETED from PHP

Retired postings and expired event pages should send URL_DELETED through the same publish endpoint with type changed. Use it only for URLs you control that now return 404 or 410, or that show an expired notice with appropriate noindex handling. Do not use URL_DELETED for moved content. For moves, publish URL_UPDATED for the new canonical and rely on 301 redirects plus sitemap updates to consolidate signals. Matching notification type to page reality keeps coverage clean and avoids extra recrawls.

<?php
$res = publish_url($token, 'https://example.com/jobs/closed-role-123', 'URL_DELETED');
echo 'HTTP ' . $res['http'] . PHP_EOL;
echo substr($res['body'], 0, 1000) . PHP_EOL;

Preflight deletions as carefully as publications. Fetch the URL, confirm status code, check robots and canonical, and confirm structured data reflects closed state where applicable, such as validThrough in the past for JobPosting. A URL_DELETED for a page that still returns 200 indexable contradicts signals and wastes crawl attention. Update the page first, then notify. For bulk closures, pace like updates, one every 30 to 60 seconds, prioritized by impressions and stale indexed snippets. Low traffic expired pages can decay via sitemap without spending quota.

Record deletions with explicit type in the same queue and log schema as updates. Include closed date, last publish status, and next day metadata result. If metadata still shows URL_UPDATED after a URL_DELETED 200, wait and recheck for storage lag. Persistent mismatch usually means the two calls used different normalized strings, so compare character by character before assuming an API fault.

Error handling for 401, 403, and 429 in PHP

Branch on HTTP status with distinct actions for each class. Refresh once on 401, park without retry on 403 permission issues, back off with delay on 429 and 5xx, and treat 404 on metadata as never submitted. Retrying everything turns permission gaps into quota exhaustion and hides root causes. The table below maps statuses to PHP behavior you can implement directly in publish wrappers and queue workers.

StatusMeaningPHP actionRetry
200AcceptedLog notifyTime, mark doneNo
401Expired or wrong tokenRefresh token once, retry onceOnce
403 permission deniedMissing Search Console OwnerPark, alert owner, no auto retryNo
403 API not enabledWrong projectEnable API in correct projectNo
404 metadataNo stored notificationTreat as never submittedN/A
429Quota or rate limitExponential backoff, pause queueAfter delay
5xxTemporary server issueBackoff with jitter, limited triesYes, capped

Wrap publish calls with timeout handling, JSON error capture, and a small retry helper. The example below retries 429 and 5xx with exponential delays plus jitter, refreshes once on 401, and parks 403 for human review. Tune base delay to your quota, starting at 60 seconds for 429 and doubling to a 900 second cap. Log full error bodies at debug level with tokens redacted, and surface concise messages at info level for daily tailing.

<?php
function publish_with_retry(array $cfg, string $url, string $type = 'URL_UPDATED', int $max = 4): array {
  list($token, $exp) = fetch_access_token($cfg);
  $delay = 60;
  for ($i = 1; $i <= $max; $i++) {
    if ($exp - time() < 300) { list($token, $exp) = fetch_access_token($cfg); }
    $res = publish_url($token, $url, $type);
    $code = $res['http'];
    if ($code === 200) { return ['ok' => true, 'body' => $res['body']]; }
    if ($code === 401 && $i === 1) { list($token, $exp) = fetch_access_token($cfg); continue; }
    if (in_array($code, [429, 500, 502, 503], true) || $code === 0) {
      sleep($delay + random_int(0, 10));
      $delay = min($delay * 2, 900);
      continue;
    }
    return ['ok' => false, 'http' => $code, 'body' => substr($res['body'] ?? $res['error'] ?? '', 0, 500)];
  }
  return ['ok' => false, 'http' => 'max-tries', 'body' => 'max tries reached'];
}

JWT failures happen before any publish HTTP, so test token exchange in isolation. Failed to parse JWT, invalid signature, and invalid grant usually mean corrupted key, edited newlines, wrong email, or clock skew. Re-download the JSON, verify client_email matches Search Console exactly, confirm scope string, and sync NTP. Do not add broad IAM roles to fix signing. For a full code by code reference, see our guide to Indexing API errors including 403, 429, and JWT failures.

Queueing and pacing bulk submissions in PHP

Bulk work needs a persistent queue with pacing, not a loop over thousands of URLs. A JSON lines file works for small sites. SQLite or MySQL suits larger catalogs. Required fields are url, type, priority, attempts, next_retry, last_status, and last_response. A worker selects due rows ordered by priority and age, publishes one at a time with sleep, updates the row, and pushes next_retry forward on 429 with exponential delay. Persistence ensures crashes do not lose progress and restarts resume cleanly.

Pace to your actual quota from Cloud console. To automate indexing php bulk work safely, target 150 publishes on a 200 quota to leave room for retries. One request every 45 to 60 seconds spreads 150 calls over about two hours. A steady php seo automation worker with one process, flock locking, and exponential backoff beats parallel loops that trigger 429. Avoid parallel workers sharing one quota without a shared lock, because concurrency multiplies rate and triggers 429 faster than logs explain. Use flock for file queues or SELECT FOR UPDATE for database queues to enforce single worker behavior. For large backlogs of thousands of URLs, window intake by date. Load 150 per day in priority order, schedule the rest forward, and let yesterday retries join today window. This keeps next_retry meaningful and prevents an overdue pile that all looks urgent.

Prioritize by business impact and structured data type. New job postings and closing date updates outrank evergreen category pages. Upcoming livestreams outrank old replays. For general hint use, new or substantially updated pages outrank typo fixes. Tag each row with priority and content type, then order selection accordingly. When quota exhausts midday, the most time sensitive URLs already went through. Quota dashboards and alert patterns are best tracked in your Cloud console Quotas page, with daily reconciliation against PHP log counts.

WordPress integration patterns without a plugin

WordPress sites can call the same PHP library from theme functions or a small must use plugin, without installing a public indexing plugin. Hook into publish and update actions, append the canonical URL to the queue, and let a scheduled worker drain with pacing. Do not call the API synchronously inside post save, because token or API latency slows the editor and traffic spikes can burst quota. Fast local intake plus paced background output keeps authoring responsive and submissions reliable.

The snippet below shows the intake half. It runs on publish, checks post type and status, resolves the canonical permalink, normalizes it, and appends a JSON line to the queue. It does no HTTP to Google, so saves stay fast even if the API is slow. A separate CLI worker or WP Cron job reads the queue with pacing and backoff. Keep the library file outside the theme if you change themes often, for example in wp-content/mu-plugins/indexer/, so updates do not remove indexing logic.

<?php
add_action('publish_post', function ($post_id) {
  if (wp_is_post_revision($post_id)) { return; }
  $url = get_permalink($post_id);
  if (!$url) { return; }
  $row = ['url' => $url, 'type' => 'URL_UPDATED', 'priority' => 10, 'attempts' => 0, 'next_retry' => time(), 'last_status' => 'queued'];
  file_put_contents('/opt/indexer-php/urls/queue.jsonl', json_encode($row) . PHP_EOL, FILE_APPEND | LOCK_EX);
}, 10, 1);

Handle WordPress specifics in the worker. Resolve canonicals with get_permalink at intake, not at drain, to avoid drift if slugs change. Skip drafts, private posts, password protected posts, and noindex pages at intake with explicit checks. On trash or delete, enqueue URL_DELETED only after confirming the front end returns 404 or 410. For custom post types such as jobs or events, hook publish_{post_type} separately with appropriate priority values. Test on staging with three posts, publish, update, and trash, and confirm queue rows, publish statuses, and next day metadata before enabling on production.

Schedule the drain with WP Cron for small sites or system cron for larger ones. WP Cron depends on site visits and can stall on low traffic sites, so system cron calling wp-cron.php or a CLI drain script is more reliable for quota pacing. Use a lock to prevent overlap, log to a restricted file, and alert on repeated 403s that indicate missing delegation for new sections. Cron and hook details overlap with our broader WordPress walkthrough, and the same queue schema can be shared with Node or Python workers in mixed teams using a shared table and per stack auth.

google indexing api php diagram: prerequisites and key setup, building and signing jwts, checking status with getmetadata <!-- IMAGE-PROMPT workflow-02: 1600px max, DependsIt brand, subject: PHP queue and WordPress hook workflow showing post publish hook then queue file then paced CLI worker then logs, flat vector, accessible, no em dash, mint #22E3B0 on charcoal #121212, node-network line art, Clash Display style headings, General Sans labels -->

Logging, security, and maintenance for PHP workers

Log one JSON line per attempt with timestamp, normalized URL, type, HTTP status, notifyTime on success, error code and message on failure, attempts, and worker ID. JSON lines work with jq, SQLite imports, and log aggregators. Keep web accessible logs disabled. Store logs outside the web root with mode 640 for the service group, rotate daily, and retain thirty days. Include project ID and service account email in the run header so multi client hosts can separate activity. Never log full tokens or private keys. Prefixes and request IDs are enough for correlation.

Secure the key and the queue with the same care as database credentials. Key file at mode 600 for the runtime user, queue directory writable only by the worker, logs readable only by operators. On shared hosting, add deny rules for secret directories, disable directory listing, and avoid temp copies in /tmp with predictable names. In code, load the key once per run, keep it in memory, and avoid embedding it in exception messages sent to external trackers. Redact Authorization headers before logging request dumps. These habits keep debugging useful without turning logs into a leak.

Maintain on a schedule. Rotate keys every 90 to 180 days with a zero downtime swap. Create the new key, deploy to staging, promote to production during a quiet window, monitor for 401s for a day, then delete the old key. Review Search Console Owners quarterly, remove stale entries, and confirm submissions come only from expected hosts. Reconcile PHP log counts against Cloud quota graphs daily. If logs show 120 successes but console shows higher consumption, look for duplicate workers or manual tests sharing the project. If console shows fewer, check for proxy blocks or timeouts where requests never left. For quick manual verification alongside PHP automation, keep cURL and Postman spot checks handy with a fresh token and one test URL. For official scope and auth details, see Indexing API prerequisites and using the Indexing API.

Checklist before you scale PHP submissions

Before you increase daily volume, walk through identity, quota, content, queue, and monitoring in order. Start with identity and key placement. Confirm the service account email logged at startup matches the Owner entry for each Search Console property, including domain and prefix variants you will submit for. Confirm the JSON key ID in Cloud matches the file or secret version on production, with no unknown active keys remaining. Verify INDEXING_KEY_PATH resolves for both CLI and web users where applicable, with mode 600 and ownership set to the runtime user. On shared hosting, confirm deny rules protect secret directories and that directory listing is disabled. These checks prevent scale failures where manual tests as one user succeed but background workers as another user fail.

Next, lock quota and pacing to real console numbers. Record publish quota per day, metadata read quota, and per minute limits from Enabled APIs then Quotas. Set worker targets near 75 percent of publish quota to leave room for retries and manual spot checks. Confirm sleep intervals of 45 to 60 seconds between requests, exponential backoff for 429 and 5xx up to 900 seconds with jitter, single retry for 401 after refresh, and park without retry for 403. Run a ten URL dry run in priority order and confirm one JSON log line per attempt with timestamp, normalized URL, type, HTTP status, notifyTime on success, and error body on failure. Reconcile counts against console graphs that evening and look for duplicate workers or forgotten cron entries if numbers diverge.

Then confirm content quality and downstream tracking. Verify sample URLs return 200, allow crawling, have no noindex, use self referencing canonicals, and carry valid JobPosting or BroadcastEvent markup where you claim documented scope. Confirm queue fields for url, type, priority, attempts, next retry, last status, and last response, with date windowing so only today window is due. Set alerts for repeated 403s and early day 429s, routed to the runbook owner with redacted excerpts. Sample ten URLs for URL Inspection and server log checks for Googlebot after notifyTime. Raise volume stepwise, for example 20 to 80 to 150 per day, pausing between steps to observe error rates and coverage movement before the next increase.

FAQ

Does an indexing api php script need Composer packages?

No, a lean indexing api php script runs on core OpenSSL, JSON, and cURL alone, which suits shared hosting where Composer updates are painful. Core functions build JWTs, exchange tokens, and POST notifications with visible timeouts and retries. Helpers like the php google client library or the broader google api php client can simplify JWT building and token caching, but each adds version work. Start with core helpers centralized in one library file, including base64url encoding and cURL wrappers, and add packages only when you need features like automatic refresh across many workers. Pin PHP minor version and test upgrades in staging first.

Why does php jwt google token exchange fail with invalid signature?

Most php jwt google failures trace to base64url mistakes, edited private_key newlines, wrong client_email, clock skew, or scope typos with trailing spaces. Re-download the JSON to rule out corruption, use one base64url helper everywhere, confirm the exact indexing scope, and sync time with NTP. Test fetch_access_token alone before debugging publish logic, printing only token prefix and expiry. Do not add IAM roles to fix signing, since roles never repair signatures. Log key ID and service account email at startup, keep the key at mode 600 for the runtime user, and compare the deployed file against Cloud key IDs when doubts remain.

How do I handle 403 permission denied from a php index script?

The service account email must be Owner on the exact Search Console property containing the URL. Open Users and permissions for that property, confirm exact email and Owner status, and watch for domain versus prefix mismatches that cause most 403s. Park the URL in your php index script queue without retry until delegation is fixed, then retry once with a fresh token and the exact canonical string. Also confirm the API is enabled in the same project that owns the key. Each php url notification that returns 403 should record property, email, timestamp, and error body, then notify the property Owner rather than looping retries that pollute logs.

Can I php submit url to google directly from WordPress post save?

Queue at save and drain in background instead of calling synchronously. Direct calls slow the editor on token or API latency and risk quota bursts on traffic spikes when many posts update at once. To php submit url to google reliably, append the canonical permalink to a queue file on publish_post with no HTTP to Google, then process with a paced CLI worker or reliable cron with locks and backoff. Keep the library outside the theme, for example in mu-plugins, so theme changes do not remove logic. This fast intake plus paced output pattern keeps authoring responsive while submissions stay quota safe and fully logged.

How many URLs can I send per day with php seo automation?

Your Cloud project quota decides, often near 200 publishes per day for new projects with separate read limits. A steady php seo automation worker targets about 75 percent of that number to leave room for retries and manual spot checks. To automate indexing php volume safely, pace one request every 45 to 60 seconds, prioritize new postings and material updates over typo fixes, and window large backlogs across days with 150 due per day in priority order. Log timestamp, URL, type, status, and notifyTime for every attempt, reconcile PHP counts against console graphs each evening, and pause on 429 until reset rather than retrying tightly.

Should PHP workers also handle IndexNow alongside indexing api curl php calls?

Keep the two systems separate with separate keys, endpoints, logs, and quotas, even when one codebase handles both. Use the indexing api curl php flow in this guide for Google with service account JSON and Search Console delegation, and run IndexNow in parallel for Bing, Yandex, Naver, and Seznam with its own key file and submission flow. Google does not support IndexNow, so mixing credentials or logs creates confusion during incidents. When you need both engines, share only the normalized URL queue schema, then let per engine workers apply their own auth, pacing, and monitoring with clear project separation.

Sources

  • https://developers.google.com/search/apis/indexing-api/v3/prereqs
  • https://developers.google.com/search/apis/indexing-api/v3/quickstart
  • https://developers.google.com/search/apis/indexing-api/v3/using-api
  • https://support.google.com/webmasters/answer/9008080
  • https://www.indexnow.org/documentation

Further reading

Put this into practice. Indexer submits URLs to the Google Indexing API and IndexNow, audits coverage with Search Console, and shows exactly which pages are indexed. Start free or see how it works.