ToolSura
    ToolSura
    HomeTools
    Blog
    ToolSuraPrivacy-First Tools

    Free utilities that run in your browser. No trackers, no accounts, no uploads.

    All Systems Operational

    Product

    • Free Online Tools
    • Contact
    • FAQs
    • About

    Legal

    • Privacy Policy
    • Cookie Policy
    • Terms & Conditions

    Resources

    • Blog
    • Brand
    • Help

    Social Links

    • Bluesky
    • Mastodon
    • X
    • Product Hunt
    • GitHub
    • LinkedIn
    • DEV.to
    • YouTube

    © 2026 ToolSura. Free tools that run in your browser.

    Remote-First / Based in India

    Technical Manifesto

    Private • Client-Side • No Uploads

    ToolSura on Nick Launches
    Browser-Native
    Privacy-First
    Skip to main content
    Toolsura
    developer-tools
    M
    Marcus Oyelaran

    The 403 that isn't about your credentials

    October 10, 2026 · 10 min read

    A 403 from the Google Indexing API is usually not a credential problem. The five failures behind it, and why reason beats status code.

    A terminal showing a JSON error response with a 403 status and the message 'Permission denied. Failed to verify the URL ownership.' beside two browser windows, one showing a job posting page and one a livestream watch page, the only two page types the Indexing API will accept.
    A terminal showing a JSON error response with a 403 status and the message 'Permission denied. Failed to verify the URL ownership.' beside two browser windows, one showing a job posting page and one a livestream watch page, the only two page types the Indexing API will accept.

    The exact string on your screen, in full, is the diagnosis:

    Permission denied. Failed to verify the URL ownership.

    You have a service account. Its key is valid, the access token is fresh, the API is switched on, and you added the account as a delegated owner of the property. Every step on Google's prerequisites page is done. The call still returns 403, and most people conclude the credential is wrong. That conclusion costs an afternoon of key rotation.

    That conclusion is wrong in a checkable way. Google's error reference defines exactly one Indexing-specific 403 message, and it is about the URL, not the token. A malformed, expired, or unauthorised credential fails at 401, with reasons unauthorized, authError, or expired, and never reaches 403.

    There is also no troubleshooting page to find. /troubleshooting on the Indexing API documentation returns 404, and everything about this failure sits on one error page, beside two dozen global reasons sharing its status code.

    This post assumes your credential works. If it does not, setting up the service account is the previous step.

    Google's error reference has two layers, and only the second one is specific

    The Indexing API error reference explains its own structure up front. The errors it lists first "are in the global, or default, domain for Google APIs", while "Many APIs also define their own domains, which identify API-specific errors that are not in the global domain." Which layer spoke is recorded in the body, as "the value of the domain property in the JSON response".

    Below the heading Indexing API-specific errors, the page promises a short list and prefaces it with: "In all cases below, the request was rejected and Google doesn't crawl the URL." The entire FORBIDDEN table on that page is one row:

    Error message Description
    Permission denied. Failed to verify the URL ownership. User did not complete the Ownership Verification process or is trying to update a URL that they do not own.

    That is the complete set. Every 403 the API raises on its own behalf is an ownership verdict. The other 403 reasons you will find (forbidden, accessNotConfigured, insufficientPermissions, quotaExceeded) come from the global domain, carrying "domain": "global" in the body. Credentials have their own status code, one step down: the UNAUTHORIZED section lists unauthorized, authError, and expired, each described in terms of the Authorization request header. None of them is the error you are reading.

    Read reason, not the status code

    A Google API error body carries two layers of detail, and developers reliably read only the top one:

    {
      "error": {
        "errors": [
          {
            "domain": "global",
            "reason": "quotaExceeded",
            "message": "The requested operation requires more resources than the quota allows."
          }
        ],
        "code": 403,
        "message": "The requested operation requires more resources than the quota allows."
      }
    }
    

    code is 403 and the top-level message is a quota message. Anything that branches on the status code stops there and concludes, wrongly, that the call failed on permissions. The classification lives in error.errors[0].reason, and the layer that answered lives in error.errors[0].domain.

    Log reason on every non-2xx response and branch on that, never on code. Every failure below has a distinct reason string, and two of them share a status code with something else entirely.

    Five failures, one status code

    # Cause Status reason or message Documented in
    1 Page is not JobPosting or BroadcastEvent in a VideoObject 403 Permission denied. Failed to verify the URL ownership. quickstart, using-api, quota, core-errors
    2 Service account is not a delegated owner 403 same message as row 1 prereqs
    3 URL is not under a verified Search Console property 403 same message as row 1 core-errors
    4 Quota exhausted 429 Insufficient tokens for quota 'indexing.googleapis.com/default_requests' core-errors
    5 Indexing API not enabled on the project 403 accessNotConfigured core-errors

    Rows 1 to 3 collapse into one string deliberately. Google does not tell you which of the three applies.

    Row 1: the page is not the kind of page this API can touch

    This sits behind most of the tickets, and it is the one Google states most clearly while marking least prominently. From the quickstart:

    "The Indexing API can only be used to crawl pages with either JobPosting or BroadcastEvent embedded in a VideoObject."

    The same sentence appears on How to Use the Indexing API, in identical wording, and a third time on the quota and approval page, where its function changes:

    "To request quota beyond the initial default quota and gain approval to use the API for pages with JobPosting or BroadcastEvent markup, fill out this form. You'll need to know the details of your project in the Google Cloud console. The quota may increase or decrease based on the document quality."

    On the first two pages that reads as a description of scope. On the third it is an entitlement condition, and document quality is the deciding variable. A developer collecting 403s has not broken authentication. They have failed a test.

    The prerequisites page lists four steps (project, service account, site owner, access token), and the type restriction appears on none of them.

    Rows 2 and 3: who is calling, and for which URL

    Row 2 has a known fix. Delegated ownership takes two moves, per the prerequisites page: "First prove that you own the site, using Search Console, then Add your service account as an owner." The identity has a fixed shape, my-service-account@project-name.google.com.iam.gserviceaccount.com. Adding the wrong address, adding a human user, or granting access against a different property leaves you holding row 3's problem inside row 2's clothes.

    Row 3 is subtler, because "I own the domain" and "the URL sits inside the property my account owns" are different sentences. Search Console's ownership documentation draws the line: "Verifying ownership of a root domain automatically verifies ownership of all subdomains, but verifying ownership of a subdomain does not verify ownership of a parent domain." Verify https://jobs.example.com as a property and you have not verified https://example.com/jobs/42. URL-prefix properties are exact prefixes; Domain properties cover every protocol and subdomain beneath them.

    One rule people carry as settled fact has no documentation behind it: that the Search Console property and the service account must live in the same Cloud project. Google's Indexing API documentation never says it. The documented requirement is delegated ownership of the property covering the URL, and nothing constrains which project holds the account.

    Row 4: quota is a 429, and sometimes a 403 anyway

    The API's own documented quota failure is a 429:

    | Insufficient tokens for quota 'indexing.googleapis.com/default_requests' | User is exceeding their Indexing API quota. |

    Two consequences. The default allowance is 200 publish requests per project per day, and "The daily quota resets at midnight Pacific Time, which means new quota can take up to 24 hours to become effective." A developer who exhausts it at 09:00 in Asia has spent the day; there is no partial refill. And the status code fails you anyway, because the global 403 list independently carries quotaExceeded, dailyLimitExceeded, dailyLimitExceededUnreg, limitExceeded, rateLimitExceeded, userRateLimitExceeded, and servingLimitExceeded. All seven arrive as 403.

    Quota is therefore a 429 or a 403 depending on which layer answered. Branching on the code walks a quota problem down the permissions path, where you re-examine a credential that was never broken.

    Row 5: accessNotConfigured is three different problems

    The global 403 list carries that reason string three times, under three unrelated descriptions: the project is not configured to access the API, the project "has been blocked due to abuse", or the project "has been marked for deletion".

    An API that is not enabled is a five-minute fix. A blocked project is an appeal. A project marked for deletion is terminal. The reason string is identical in all three, which makes this the one place where reason alone is not enough.

    Prove eligibility before you send anything

    The cheapest diagnostic here costs one HTTP request. Fetch the page and read its JSON-LD for a node with "@type": "JobPosting", or a "BroadcastEvent" node nested inside a "VideoObject" node. The JobPosting documentation is where that markup is specified.

    The nesting is literal. schema.org defines BroadcastEvent as "An over the air or online broadcast event" and types it as a subtype of PublicationEvent, a thing that happened rather than a thing that is a video. "Embedded in a VideoObject" means the event node sits inside the video node, recording when that video aired. A page carrying a BroadcastEvent on its own, with no video attached, has not met the condition.

    Check rendered source rather than the template: structured data injected after load is absent from the HTML your client received. If neither source nor the Rich Results Test finds an eligible node, stop debugging the API. The request is correct and the page is ineligible, which is row 1 and always has been.

    What to send, exactly

    Element Value
    Endpoint POST https://indexing.googleapis.com/v3/urlNotifications:publish
    Content-Type application/json, required by the docs for all calls to that URL
    OAuth scope https://www.googleapis.com/auth/indexing
    Query parameters none. The discovery document declares "parameters": {}
    Body {"url": "https://example.com/jobs/42", "type": "URL_UPDATED"}

    All of it comes from the API discovery document and the using-api page. That empty "parameters": {} is worth pausing on: publish accepts no options, so there is nothing to toggle and no flag to set. The REST reference also documents a notifyTime field and states: "Users should not specify it, the field is ignored at the request time."

    The url field's own description carries the whole failure in one word: "The URL must be owned by the publisher of this notification and, in case of URL_UPDATED notifications, it must be crawlable by Google." Owned. Not permitted, not authorised. Owned: the word the error message uses.

    URL_REMOVED is not a value you can send

    There is a real inconsistency in Google's documentation here, and copying from the wrong page produces a 400 you cannot explain afterwards.

    The Indexing-specific BAD_REQUEST table on the errors page says the notification type "is required and must be 'URL_REMOVED' or 'URL_UPDATED'", and the companion row blames a 400 on setting "the notification type to something other than 'URL_REMOVED' or 'URL_UPDATED'".

    The REST reference for urlNotifications lists a different enum: URL_NOTIFICATION_TYPE_UNSPECIFIED ("Unspecified"), URL_UPDATED ("The given URL (Web document) has been updated"), and URL_DELETED ("The given URL (Web document) has been deleted").

    URL_DELETED is what the discovery document accepts and what every example in the using-api documentation sends. URL_REMOVED appears only inside that error-message text. Copy it out of the error message, paste it into a request, and you get Invalid value at 'url_notification.type' (TYPE_ENUM).

    What this API is not

    The Indexing API is not a way to get a URL indexed. It is a freshness pipe for two content types, and for anything else the honest answer is Search Console's URL Inspection tool or patience, as covered in how long Google takes to index a page.

    Three limits that read like guarantees if you skim them:

    • A 200 means Google "may try to recrawl this URL soon." May. It acknowledges that your request was accepted and says nothing further.
    • The GET request "doesn't tell you when Google indexes or removes a URL; it only returns whether you successfully submitted a request."
    • getMetadata is narrower still. It "can only be used to query URLs that were previously seen in successful Indexing API notifications."

    There is no inspect-and-request capability in this API. Getting a page into the index means sitemaps, internal links, or a manual request through URL Inspection.

    A long-running failure, not a recent regression

    If your 403 came out of a decade-old thread, the API has not been abandoned. The discovery document carried revision 20261007 when this was checked, and the quickstart, using-api, errors, and quota pages were last updated on 16 July 2026. No deprecation notice, no sunset date.

    The failure is at least as old. On Stack Overflow the google-indexing-api tag held 47 questions at the time of writing. Among the top 25 by votes, nine carry a 403 in the title, the oldest from July 2018 and the newest from May 2024, with view counts between roughly 2,400 and 7,800. Several are near-identical: "Google Indexing API - 403 'Forbidden Response'", "Getting a 403 error when using Google Indexing API", "403 error in Google Indexing API (after trying all solutions from other SO questions)".

    Those counts are [TREND] data: decade-accumulated evidence that a failure mode is real and long-lived, not search volume, and silent on current demand. No volume figure for this query appears anywhere in this post. The claim they support is narrow: a developer in 2019 and a developer today are hitting the same wall, and the wall has not moved.

    Related reading

    • Google Indexing API Explained (and Who Can Use It) — what the service is, who may use it, and the four setup steps. Read that first if you have not.
    • How to set up a Google service account for the API — strictly the previous step, and the answer if your failure is genuinely a 401.
    • How long does Google take to index a page? — the honest route for a page that is neither a job posting nor a livestream.
    • GSC Indexer FAQ — our own command-line wrapper, and what it does with the notifications handed to it.
    Marcus Oyelaran

    Written by

    Marcus Oyelaran

    My first move on any indexing problem is to check what the server sends back, because the setting in the content system and the response the crawler received are frequently different things. There are two ways to say do not index, and both have to be in agreement. A robots meta tag in the page, and a header carrying the same instruction.

    The header takes precedence, so a page with a noindex tag and a missing header from a misconfigured rule will still be indexed, which is the single most common cause of a page that refuses to leave a result page. A page excluded through the robots file cannot be crawled, so its meta tag is never read, so it cannot be indexed that way either. The file and the tag solve different problems: the file prevents crawling, the tag prevents indexing while allowing the crawl.

    Removing a page from a sitemap does not remove it from an index. The sitemap is for discovery. If a page needs to leave an index it needs the instruction, and the instruction takes a crawl to be seen.

    Deletion is the slowest path, since removal depends on a crawler revisiting the address, which is an argument for keeping low-value pages rather than deleting them and hoping. I also cover the case that looks like an indexing bug and is not: a page indexed at a different address than the one requested, which is a canonical problem. Each of these is settled by the response the crawler received, so I show that response rather than the configuration that produced it, since the two diverge more often than the tooling suggests.

    Share

    Frequently Asked Questions

    Previousrobots.txt noindex: Why Google Never Reads the DirectiveNextPOST vs PUT vs PATCH: What Actually Distinguishes Them

    Comments

    Leave a Review

    Rate this tool
    Overall Rating
    Spam Protection Active