Schema Markup Validator API: Checking Structured Data in CI

A schema markup validator answers one narrow question: does the markup on this page carry the fields its declared type requires? Google retired the API behind its own structured data testing tool, and validator.schema.org rate limits automated traffic, so there is nowhere left to call from a build. Here is how to get the check back into continuous integration, and what it can honestly tell you.

What happened to Google's structured data testing tool API

The Structured Data Testing Tool had an endpoint you could call from a script or a deploy hook. It was retired. Its replacement, the Rich Results Test, lives inside Search Console, runs in a browser, and has no documented public API. The generic markup validation was handed to the schema.org community and now lives at validator.schema.org.

A check that ran in a deploy script became something a person does by pasting a URL into a form. Most teams did not replace it, so structured data quietly rotted: a template change drops address from the LocalBusiness block, nobody notices for months, and the first sign is someone asking why the map card disappeared.

Two different things get confused whenever this comes up. Checking that markup parses and carries its required fields is deterministic: the requirements are published, so any implementation gets the same answer. Predicting a rich result is not, because that decision belongs to the search engine. Only the first is an API problem.

Why validator.schema.org is rate limited

The public validator is a free service run for people. It answers one document at a time from a browser and throttles automated traffic, as every free public service eventually must. Pointing a CI job at it means every build you run competes with everyone else's.

When that service answers 429, the correct response is to back off and stop. Not to retry from a different address, not to spread the same job across a pool of runners. Locomint takes that as a hard rule for every fetch it makes, and the same rule applies to how you should treat someone else's service: a rate limit is an answer, not an obstacle. The piece on the legal position of public data goes into where that line sits.

What a schema markup validator can honestly check

Vendors in this space are vague about the contract, so be precise about it. Six questions, and only four of them have an answer that does not depend on who you ask.

QuestionCheckable?Why
Does every JSON-LD block parse, with an @context?YesPure syntax; the answer is the same for everyone
Which schema.org types does the page declare?YesRead from @type across every block and nested object
Are any documented required fields missing?YesThe requirements per type are published and stable
Are recommended fields missing?YesSame source; reported separately because they never break anything
Are the values true — is the rating real, are the hours current?NoNothing in the markup can prove that. It is a data problem, not a validation one
Will this page get a rich result?NoThe search engine decides, using signals that are not published

The validate endpoint reports the first four and refuses to guess at the last two. It returns blocks, types, checked_types, an items array, an errors array and a single valid verdict. Each item names its type, the ruleset it was checked_against, and its missing_required and missing_recommended lists.

checked_against matters more than it looks. Schema.org has hundreds of business subtypes; search engines document requirements for far fewer. A Dentist is judged against LocalBusiness, because that is the ruleset that exists, and checked_types names the rulesets actually applied — so a type nobody documents does not silently pass as correct.

Three request forms: url, html and jsonld, and what each costs

The endpoint is POST /v1/schema/validate and it takes exactly one of three inputs. Which one you send changes both what the API does and what it costs.

  • url — the API fetches the page and reads every JSON-LD block in it. Use this for a live site you do not control, a competitor's page, or a spot check after a deploy. One credit.
  • html — you send the document; the API parses it without fetching anything. This is the CI form: the built file on disk, before it ever reaches a server. Free.
  • jsonld — you send the structured data document on its own, without the surrounding page. Useful when the markup is generated by application code and you want to test the generator rather than the template. Free.

The live-URL form, with curl:

curl -X POST "https://api.locomint.io/v1/schema/validate" \
  -H "X-API-Key: $LOCOMINT_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/product"}'

Authenticate with the X-API-Key header on every call, including the free ones: a request without a key is a 401 whether or not it would have cost anything. A key from the signup page works immediately with 200 credits a month and no card.

Why checking built HTML in continuous integration is free

The credit rule is simple: you pay when the API goes out to the network on your behalf. Fetching a URL means a request, a page download and a parse. Sending html or jsonld means only the parse, so it costs nothing.

That changes what you can do with the check. A validator that costs a credit per page is something you run before a release, on a handful of important templates. A free one runs on every commit across every page the build produces. The rate limit still applies — 60 requests a minute on Free and Starter, higher on the paid plans on the pricing page — so validate in sequence rather than fanning out across parallel workers.

Validate the built output, not the template. The bugs that reach production are almost never in the JSON-LD you wrote by hand. They come from a variable that was empty for one product, an escaping change that broke a quote, or a CMS field renamed by someone in marketing. A template test passes on all three. Reading the rendered file catches all three.

A build step that fails on a missing required field

This script takes the built HTML files as arguments, validates each one, prints a line per file, and exits non-zero if any required field is missing or any block fails to parse. Missing recommended fields are printed as notes and never fail the build, which keeps the signal honest — a build that goes red for a missing image is a build people learn to ignore.

"""Fail the build when required schema.org fields are missing.

Usage: python check_schema.py dist/index.html dist/pricing.html
"""
import os
import sys

import httpx

API = "https://api.locomint.io/v1"
KEY = os.environ["LOCOMINT_KEY"]

failures = []
notes = []

with httpx.Client(base_url=API, headers={"X-API-Key": KEY}, timeout=60) as client:
    for path in sys.argv[1:]:
        with open(path, encoding="utf-8") as fh:
            html = fh.read()

        resp = client.post("/schema/validate", json={"html": html})
        resp.raise_for_status()
        data = resp.json()["data"]

        if not data["blocks"]:
            failures.append(f"{path}: no JSON-LD block in the page")
            continue

        for err in data["errors"]:
            failures.append(f"{path}: {err}")

        for item in data["items"]:
            missing = item.get("missing_required") or []
            if missing:
                failures.append(
                    f"{path}: {item['type']} is missing {', '.join(missing)}")
            advisory = item.get("missing_recommended") or []
            if advisory:
                notes.append(
                    f"{path}: {item['type']} could add {', '.join(advisory)}")

        print(f"{path}: {data['blocks']} block(s), "
              f"types {', '.join(data['types'])}, valid={data['valid']}")

for line in notes:
    print(f"note  {line}")
for line in failures:
    print(f"FAIL  {line}", file=sys.stderr)

sys.exit(1 if failures else 0)

Two details are worth copying. It checks data["blocks"] first, because a page with no markup at all is the failure people miss: items and errors are empty and valid is technically true, so a naive check on valid passes a page that says nothing. And it reads missing_required per item, so the message names the type and the field.

Wire it in after the site build and before the deploy step. It never spends a credit: it only ever sends html.

How a JSON-LD validator handles @graph documents

Most real pages do not carry one flat object. They carry an @graph: a single @context wrapping an array of nodes — an Organization, a WebSite, a WebPage, a LocalBusiness, a BreadcrumbList — each with an @id, and cross-referenced by those ids rather than nested inside each other. Most CMS plugins emit exactly this shape.

The validator walks the graph and reports every typed node as its own entry in items. A page with four nodes in one @graph comes back with four items, each judged against its own ruleset, rather than one item for the outer document. Nested objects are handled the same way, so a PostalAddress inside a LocalBusiness shows up in types and counts towards the parent's required fields.

One consequence to watch. When a node points at another by @id instead of embedding it, the field is present as a reference, and the reference is what gets checked. If your plugin writes "address": {"@id": "#local-address"} and the node with that id was dropped from the graph by a caching layer, the parent still looks like it has an address. This is the failure mode worth adding a specific assertion for in your own script: confirm that every @id referenced somewhere in the document is also defined somewhere in it.

blocks counts <script type="application/ld+json"> elements, not objects: two blocks holding three nodes each gives blocks: 2 and six items. Repeated entries in items — the same Organization from both a theme and a plugin — are usually the first hint that two things on the site are writing markup.

Generating the markup instead of grading it

Validation tells you a field is missing. It cannot tell you the correct value, and on a local business page that gap is most of the work: address in parts, coordinates, phone in international format, seven days of hours.

POST /v1/schema/generate covers it from the other direction. Give it a place_id and it builds LocalBusiness markup from the real record, picking the closest type automatically, and it runs its own output back through the validator before returning it. That is the subject of the LocalBusiness schema guide, and the Structured Data API page shows the full response shape for both halves.

Where to start this week

Run the url form once against your own homepage and read the types array. The usual answers: a plugin someone installed and forgot, an Organization block from the theme, or nothing at all. That one credit tells you whether you have a problem worth automating.

Then add the script to your build with the two or three page types that matter most, and let it run red once on purpose by deleting a required field so you know the failure is visible. After that it costs nothing to keep. If those are local business pages, read how the same records ground AI answers: the fields that make good markup are the fields an assistant needs too.

Frequently asked questions

Is there still an API for Google's structured data testing tool?

No. The endpoint behind the old structured data testing tool was retired, and its replacement, the Rich Results Test, runs in the browser inside Search Console with no documented public API. Validation moved to validator.schema.org, which is a free service for people rather than a build dependency. If you need the check in a pipeline you have to call a service that is meant to be called.

Can a validator tell me whether I will get a rich result?

No, and any tool that claims it can is overselling. A validator checks conformance: whether the JSON-LD parses, which types the page declares, and which documented required and recommended fields are missing. Whether a search engine shows a rich result is its own decision, based on quality signals nobody outside it can see.

Does validating markup in a build cost credits?

Not when you send the markup yourself. Checking an html or jsonld document you already hold is free, because nothing is fetched on your behalf. Checking a url costs one credit, since the API has to fetch that page. A build that validates its own output can therefore run on every commit without touching the monthly quota.