Skip to content
TurboIndex

Google Indexing API for Job Postings: The Official Use

How job boards use the Google Indexing API as intended: JobPosting markup, URL_UPDATED on publish, URL_DELETED on expiry, quotas, setup and common 403 errors.

October 11, 20266 min read

Job postings are the one case where the Google Indexing API is used exactly as documented: a job page with JobPosting structured data goes to Google with URL_UPDATED the moment it is published or edited, and with URL_DELETED the moment it expires or is filled. Google built the API for this because job listings are short-lived, and a search result that leads to a dead job is worse than no result. The rest of this article is the setup, the lifecycle, the quotas and the mistakes that job boards and company career pages make, plus the only other official case, livestream pages.

Why job pages get a direct line

Google's job search experience shows postings pulled from pages with valid JobPosting markup. Those pages change fast: a listing goes up, gets edited, and comes down in days or weeks. Sitemaps and normal crawling are too slow for that, so Google offers the Indexing API and says plainly that it can only be used to crawl pages with JobPosting or with BroadcastEvent embedded in a VideoObject.

Google also asks for the other direction. When a job is filled or expired, it wants the posting gone, and lists three ways to signal that: set validThrough to a date in the past, remove the page so it returns 404 or 410, or remove the JobPosting markup from the page. The API's URL_DELETED notification is how you tell Google to come and look.

What the page needs first

The API does nothing useful for a page that does not qualify. Each job page needs JobPosting markup with the required properties: title (the job title, not the page title), description (the full description in HTML), datePosted, hiringOrganization and jobLocation. validThrough is required for postings that have an expiry date; if a job never expires, leave it out.

A minimal JSON-LD block:

<script type="application/ld+json">
{
  "@context": "https://schema.org/",
  "@type": "JobPosting",
  "title": "Backend Engineer",
  "description": "<p>We are hiring a backend engineer to ...</p>",
  "datePosted": "2026-10-11",
  "validThrough": "2026-11-30T23:59",
  "hiringOrganization": {
    "@type": "Organization",
    "name": "Example Ltd",
    "sameAs": "https://example.com"
  },
  "jobLocation": {
    "@type": "Place",
    "address": {
      "@type": "PostalAddress",
      "streetAddress": "1 Main Street",
      "addressLocality": "Dublin",
      "addressCountry": "IE"
    }
  }
}
</script>

Validate it with Google's Rich Results Test before you wire up the API, and keep validating as templates change. One job page per URL: the API wants the page that holds one posting, not the category page that lists fifty. The structured data viewer shows what a live URL currently exposes, and the basics of markup are in structured data basics.

The lifecycle, event by event

Event on your site What the page should do API call
Job published Return 200 with valid JobPosting markup URL_UPDATED
Job edited (salary, location, description) Same URL, updated markup URL_UPDATED again
Job expired or filled Return 404 or 410, or carry noindex, or validThrough in the past with the markup removed URL_DELETED
Job reopened Same URL back to 200 with markup URL_UPDATED

Two details matter. Google's documentation says that before you request removal, the URL must return 404 or 410 or include a noindex tag; sending URL_DELETED for a page that still answers 200 with a live posting is contradictory, and Google goes with what it finds on the page. And a successful response (HTTP 200 from the API) means Google may try to crawl the URL soon; it is a request, not a confirmation of indexing or removal. Check the result through URL Inspection or its API a day or two later.

Setup in six steps

  1. Create a Google Cloud project (or reuse one owned by the company).
  2. Enable the Web Search Indexing API in the API library.
  3. Create a service account and download a JSON key.
  4. In Search Console, open the property that contains the job pages, go to Settings > Users and permissions, add the service account's email address and give it the Owner role. A delegated owner added this way is fine; Full user is not.
  5. Send a test URL_UPDATED for one live job page and read the response.
  6. Hook publish, update and expiry events in your application to the three calls.

The click path through the 2026 Cloud Console is in create a Google Indexing API key, and working Python, PHP and Node.js code is in Indexing API code examples. A single publish call in Python, for reference:

from google.oauth2 import service_account
from google.auth.transport.requests import AuthorizedSession

creds = service_account.Credentials.from_service_account_file(
    "sa.json", scopes=["https://www.googleapis.com/auth/indexing"])
session = AuthorizedSession(creds)

def notify(url, kind="URL_UPDATED"):
    r = session.post(
        "https://indexing.googleapis.com/v3/urlNotifications:publish",
        json={"url": url, "type": kind})
    return r.status_code, r.json()

print(notify("https://example.com/jobs/backend-engineer-dublin"))
print(notify("https://example.com/jobs/old-posting", "URL_DELETED"))

Quotas and how to spend them

  • 200 publish requests per day per Google Cloud project, counting URL_UPDATED and URL_DELETED together.
  • Up to 100 requests in one batch HTTP request. Batching saves connections, not quota: each URL still counts.
  • 180 metadata requests per minute and 380 requests per minute in total.
  • The daily counter resets at midnight Pacific time.

For an aggregator publishing 2,000 jobs a day, 200 is nowhere near enough, and the official route is the quota increase form Google links from its documentation, where you describe the site and the volume. Do not spread one site across several Cloud projects or Google accounts; Google's documentation names that as abuse and says it can lead to the API being revoked.

Spend the 200 on events that matter: new postings first, removals second (an expired job in results reflects on your site), edits last. Never send a URL that currently returns 404 when you meant URL_UPDATED, a redirecting URL, or a page that lost its markup. Each of those burns a request for nothing. The details of the counter and the form are in Google Indexing API quota.

Mistakes that come up again and again

403 Permission denied, failed to verify the URL ownership. The service account is not an Owner of the property, or the URL is in a different property (http instead of https, a subdomain not covered by a URL prefix property). Fix the role or use a Domain property.

403 accessNotConfigured. The Indexing API is not enabled in the Cloud project that owns the key. Enable it and retry.

Sending the listing page. /jobs/ or /jobs?page=3 has no single JobPosting, so it does not qualify. Send the detail URLs.

No markup, or markup that fails. A template change drops datePosted or hiringOrganization, the Rich Results Test starts failing, and notifications keep going out. Validate on deploy.

Forgetting removals. Boards that only send URL_UPDATED leave Google to find expired jobs on its own, which takes much longer and leaves users landing on dead pages. Wire expiry to URL_DELETED.

URL_DELETED while the page is still live. Make the page 404, 410 or noindex first, then notify.

Resending every job every day. Repeated notifications for unchanged content do not get a page crawled faster, and you run out of quota by noon. Send on change only.

429 Quota exceeded. You hit 200. Queue the rest for after midnight Pacific; do not retry in a loop.

The other official case: livestreams

The second page type the API accepts is a livestream page with BroadcastEvent embedded in a VideoObject. The logic is the same: a stream is announced, goes live, and ends, and Google wants to show and then stop showing it in the right window. Send URL_UPDATED when the stream is scheduled and when it starts, and URL_UPDATED again (or URL_DELETED if the page goes away) when it ends and the markup changes. Everything else in this article applies unchanged.

Doing this without writing the integration

If the job board runs on a CMS where hooking publish and expiry events is awkward, TurboIndex supports job sites with the owner's own Indexing API key: you create the project and the service account as above, upload the JSON key, and it reads the sitemap, fetches each job URL with TurboIndexBot first so a 404 or a noindex page is never sent as URL_UPDATED, submits new and changed pages, and then checks each URL through the URL Inspection API to show what Google did. The quota and the ownership stay with you, which is how Google wants it.

Frequently asked questions

Is the Indexing API only for job postings?
Officially it is for two page types: pages with JobPosting structured data and livestream pages with BroadcastEvent embedded in a VideoObject. Google's documentation says the API can only be used to crawl those pages. It accepts other URLs technically, but that use is unsupported and can lead to access being revoked.
Do I have to send URL_DELETED when a job expires?
Google asks you to remove expired job postings, and the Indexing API is the fastest way to tell it. First make the page return 404 or 410, or add noindex, then send URL_DELETED. Google's docs say the URL must already return 404 or 410 or carry noindex before you request removal.
How many job URLs can I send per day?
The default quota is 200 publish requests per day per Google Cloud project, counting URL_UPDATED and URL_DELETED together, with up to 100 requests per batch. It resets at midnight Pacific time. A quota increase is requested through Google's form, and using several projects for one site to get around it is named as abuse in Google's documentation.
Why do I get 403 Permission denied with a valid key?
The service account is not an Owner of the Search Console property that contains the URL. Add its email address under Settings, Users and permissions with the Owner role, and check that the URL matches the property (same protocol, same host). Full user permission is not enough.

Keep reading