# Schema Markup Validator - JSON-LD Structured Data Checker (`tidytools/structured-data-validator`) Actor

Validate JSON-LD and Microdata schema markup on a URL list, a sitemap or a whole site against Google rich result rules (Product, FAQ, Recipe, Event, Job...), plus Open Graph. $2 per 1,000 pages.

- **URL**: https://apify.com/tidytools/structured-data-validator.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** SEO tools, Developer tools, Automation
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 validated pages

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Schema Markup Validator do?

**Paste a list of page URLs (or a sitemap, or a site to crawl) and get one row per page: is the schema.org JSON-LD and Microdata valid for Google rich results, with stable issue codes for every problem. $0.002 per validated page ($2 per 1,000); pages that cannot be read are free.**

**Try it on 5 to 10 pages first:** that costs about $0.01 to $0.02 and takes under a minute. Then switch on *Validate the pages in the sitemap* for the whole site.

**Schema markup validator for many pages at once**: it extracts and validates schema.org **JSON-LD and Microdata** against Google rich result rules, with **stable issue codes**. A bulk alternative to Google's Rich Results Test and validator.schema.org, which check one page at a time.

It checks what Google needs for rich results: stars, prices, breadcrumbs, recipes, events, job postings and more. It also checks the **Open Graph and X (Twitter) tags** that control how shared links look. Check a list of URLs, every page in your **sitemap**, or crawl a whole website, then export every problem as a table with a **stable issue code**.

- ✅ **Rich result checks** for Product, Product variants (ProductGroup), Offer, Review and AggregateRating, Article, Breadcrumb, FAQ, Q\&A, Recipe, Event, JobPosting, LocalBusiness (and 60 subtypes such as Restaurant or Dentist), Organization, WebSite, VideoObject, SoftwareApplication, Course, Dataset
- 🧪 **Value checks** that break rich results silently: dates that are not ISO 8601, prices like `$1,299.00`, currencies that are not ISO codes, invalid `availability`, relative image URLs, bad durations, offers without `priceCurrency`, ratings outside their scale. A bad format in a required property is an error; in an optional property (e.g. `datePublished` on a Recipe) it is a warning and does not remove rich result eligibility
- 🏷️ **JSON-LD and Microdata**: Microdata (`itemscope`/`itemprop`) is validated with the same rules and marked `format: "microdata"`; RDFa types are listed in `rdfaTypes`
- 🧩 **Understands real-world markup**: `@graph` (Yoast, Rank Math, Shopify), `@id` references, nodes that share an `@id` (merged like Google does, also across separate script blocks), arrays, variants on separate pages
- 🚫 **Catches broken blocks**: invalid JSON, missing `@context` or `@type`
- 🍞 **Breadcrumb checks**: missing names or URLs, and positions that do not run 1, 2, 3…
- 🔖 **Stable issue codes** (`REQUIRED_MISSING`, `INVALID_PRICE`, `INVALID_DATE`, `RELATIVE_URL`, `INVALID_JSON`…) with severity, type, property and path, ready to filter in a spreadsheet
- 📣 **Social tags**: `og:title`, `og:description`, `og:image` (missing or relative), `twitter:card` and canonical, returned with every page
- 🗺️ **Sitemap mode**: validate every page listed in your XML sitemap, filtered by URL pattern (e.g. only `/products/`)
- 📊 **Site summary**: which types you use, which rich results are eligible, and the most frequent issues across all pages
- 📦 **Also an extractor**: optionally returns the raw JSON-LD of every page
- 💸 **$2 per 1,000 pages**, and pages that cannot be read are free

### Who is it for?

- **SEO specialists and agencies**: audit structured data across hundreds of product, article or location pages at once, instead of pasting URLs into a testing tool one by one
- **E-commerce teams**: make sure every product page keeps its price, availability and review markup after theme or plugin updates
- **Developers**: add a structured data check to your release process or schedule it weekly
- **Data teams**: extract product, recipe, event or job data that sites publish as JSON-LD or Microdata

### How much does it cost?

| Event | Price |
|---|---|
| Validated page | **$2 / 1,000 pages** |

**Example:** validating 2,000 pages costs 2,000 × $0.002 = **$4.00** on the Free plan.

**No start fee.** Pages without structured data are still checked and charged (you learn that they have none). Pages that cannot be read (blocked, 4xx/5xx, not HTML) are reported with an `errorType` but **not charged**. Higher Apify plans get volume discounts (see the *Pricing* tab).

#### Control your cost

- **Charged:** one `Validated page` event per page read (`charged: true`), including pages without structured data (`status: "none"`): knowing a page has none is the result. The **Pages in the results** setting only filters the dataset; every checked page is charged and counted in `SUMMARY`.
- **Free** (`charged: false`, the `error` ends with "(not charged)"): invalid input lines, including private or local addresses such as `localhost` or `192.168.x.x` (`errorType: "invalid_input"`), duplicate URLs (checked once; the row lists every original line in `inputs`) and pages that cannot be read (blocked, 4xx/5xx, unreachable site or bad SSL certificate, not HTML, timed out, or disallowed by the site's robots.txt: `errorType: "robots_disallowed"`).
- **Before it starts**, the run logs its plan (pages × price, with **Max pages** as the upper bound when crawling) and compares it with your **maximum charge per run** (run options). The plan is saved as `costPlan` in `SUMMARY`.
- **When the maximum is reached**, the run stops: the status message says so, `SUMMARY.status` is `LIMIT_REACHED` and `SUMMARY.notProcessed` has the number of pages not checked and the first 100 of them.
- **If Apify restarts the run** (server migration or Resurrect), pages already checked are skipped and not charged again (`SUMMARY.resumedSkipped`). In crawl and sitemap mode the crawl continues from where it stopped.

### How to use it

1. Paste the **page URLs** to check in **URLs or domains**, one per line (for example a few product, article and category pages). A bad line never stops the run: it becomes a row with `errorType: "invalid_input"` (not charged).
2. Or turn on **Validate the pages in the sitemap** (optionally with an include pattern such as `https://example.com/products/**`), or **Crawl the website**, and set **Max pages**.
3. Click **Start**. Export the results as CSV, Excel or JSON, and open the **Summary** for the site overview.

#### Input example

```json
{
    "urls": [
        "https://www.allbirds.com/products/mens-tree-runners",
        "https://www.ikea.com/us/en/p/billy-bookcase-white-00263850/",
        "stripe.com"
    ],
    "crawl": false,
    "report": "all",
    "includeJsonLd": true
}
```

Validate every product page in a sitemap:

```json
{
    "urls": ["https://www.allbirds.com/"],
    "useSitemap": true,
    "includeUrlPatterns": ["https://www.allbirds.com/products/**"],
    "maxPages": 500
}
```

Use `urls` (a plain list of strings) in API calls. The older `startUrls` field (`[{ "url": "https://…" }]`) still works and can hold uploaded or linked URL list files, but Apify rejects the whole request with HTTP 400 if one of its entries is a bare domain or a blank line.

**Pages in the results** (`report`): `all` (default), `problems+missing` (errors, warnings **and pages without structured data**, which are usually the pages you are looking for) or `problems` (errors and warnings only). The old `onlyProblems: true` means `problems+missing`. `SUMMARY.pagesWithoutStructuredData` lists up to 200 checked pages that have none (`pagesWithoutStructuredDataCount` counts all of them), whatever the setting.

Each page is requested exactly as you gave it (only a `#fragment` is dropped), so `https://www.python.org/about/?utm_source=newsletter` is checked and reported as is. Start pages keep your line in `inputUrl` and their position in `inputIndex`, so results map back to a spreadsheet. Each result also has `googleTestUrl`, a link to Google's Rich Results Test for the page.

The existing `richResults`, each item's `eligible`, `eligibleRichResultCount`, and `SUMMARY.richResults` use the Actor's legacy rule checks. They can still include a result from a JSON-LD block with a missing or invalid `@context`; keep these fields for compatibility, and do not treat them as proof that markup passed every check or that Google will display a rich result. The new `markupEligibleRichResultCount` counts distinct rich result types that pass the Actor's rule checks **and** have no blocking error on their own JSON-LD block. Errors in other independent blocks do not lower that count. For a top-level `FAQPage`, the item also has `faqGoogleDisplayStatus`: `limited_google_decides` means the FAQ passed those markup checks but Google's site-specific display eligibility is unknown; `markup_invalid` means the FAQ failed validation. The page row lists its distinct values in `faqGoogleDisplayStatuses` (empty when there is no top-level FAQ) and has `googleDisplayEligibilityUnknownCount` (0 or 1 for the FAQ result type). `SUMMARY.faqGoogleDisplayStatusPages` counts pages in each state. A page with both valid and invalid FAQ items can appear in both counts. This Actor does not decide whether a site is authoritative.

#### Output example

Real results from the input above (shortened). The product page's main `ProductGroup` is valid and eligible for product variant results, but a second JSON-LD block on the page has neither `@context` nor `@type`, so search engines ignore it:

```json
{
    "url": "https://www.allbirds.com/products/mens-tree-runners",
    "inputUrl": "https://www.allbirds.com/products/mens-tree-runners",
    "inputIndex": 0,
    "title": "Men's Tree Runners - Black - Everyday Sneakers | Allbirds",
    "status": "errors",
    "types": ["ProductGroup"],
    "richResults": ["Product variants"],
    "errorCount": 2,
    "warningCount": 0,
    "hasStructuredData": true,
    "jsonLdBlocks": 2,
    "invalidJsonLdBlocks": 0,
    "eligibleRichResultCount": 1,
    "errors": [
        "$[1]: missing \"@context\": \"https://schema.org\"",
        "$[1]: missing \"@type\""
    ],
    "warnings": [],
    "issues": [
        { "code": "MISSING_CONTEXT", "severity": "error", "type": null, "property": "@context", "path": "$[1]", "message": "missing \"@context\": \"https://schema.org\"" },
        { "code": "MISSING_TYPE", "severity": "error", "type": null, "property": "@type", "path": "$[1]", "message": "missing \"@type\"" }
    ],
    "items": [
        { "type": "ProductGroup", "path": "$[0]", "topLevel": true, "richResult": "Product variants", "eligible": true, "errors": [], "warnings": [] }
    ],
    "canonical": "https://www.allbirds.com/products/mens-tree-runners",
    "openGraph": { "og:title": "Men's Tree Runner - Jet Black (White Sole)", "og:type": "product", "og:image": "http://www.allbirds.com/cdn/shop/files/TR3MJBW080_SHOE_LEFT_GLOBAL_MENS_TREE_RUNNER_JET_BLACK_WHITE.png?v=1751165486", "...": "..." },
    "twitterCard": "summary_large_image",
    "socialIssues": [],
    "via": "backend",
    "jsonLd": [ { "@context": "https://schema.org/", "@type": "ProductGroup", "name": "Men's Tree Runner", "...": "..." } ]
}
```

Repeated problems are grouped, so a list of 24 products gives one line:

```json
{
    "url": "https://www.ikea.com/us/en/p/billy-bookcase-white-00263850",
    "status": "warnings",
    "types": ["Organization", "WebSite", "CollectionPage", "BreadcrumbList"],
    "richResults": ["Organization (logo, knowledge panel)", "Site name", "Breadcrumb"],
    "warnings": [
        "$[0].@graph[0]: Organization: missing recommended \"logo\"",
        "$[0].@graph[0]: Organization: missing recommended \"sameAs\"",
        "$[0].@graph[2].mainEntity.itemListElement[*].item.offers: Offer: missing recommended \"availability\" (x24)"
    ]
}
```

#### Microdata example (real result, September 2026)

A product page marked up only with Microdata. The price has a currency symbol and the rating has no `ratingValue`, so the product snippet is not eligible:

```json
{
    "url": "https://webscraper.io/test-sites/e-commerce/allinone/product/60",
    "status": "errors",
    "formats": ["microdata"],
    "types": ["SiteNavigationElement", "Product"],
    "richResults": [],
    "errorCount": 2,
    "warningCount": 3,
    "jsonLdBlocks": 0,
    "microdataItems": 2,
    "rdfaTypes": [],
    "errors": [
        "microdata[1].offers.price: \"price\" must be a plain number with a dot as decimal separator, no currency symbol or thousands separator: \"$295.99\"",
        "microdata[1].aggregateRating: AggregateRating: missing required \"ratingValue\""
    ],
    "issues": [
        { "code": "INVALID_PRICE", "severity": "error", "type": "Offer", "property": "price", "path": "microdata[1].offers.price", "format": "microdata", "...": "..." },
        { "code": "REQUIRED_MISSING", "severity": "error", "type": "AggregateRating", "property": "ratingValue", "path": "microdata[1].aggregateRating", "format": "microdata", "...": "..." }
    ]
}
```

Microdata paths start with `microdata[i]` (the i-th top-level Microdata item on the page) instead of `$[i]`. With **Include the raw JSON-LD** on, the extracted Microdata items are returned in `microdata` too. Turn off **Also validate Microdata** to check JSON-LD only.

#### Status values

| status | Meaning |
|---|---|
| `valid` | Structured data found, no errors or warnings |
| `warnings` | Only recommended properties are missing; the markup may still qualify for a rich result, subject to Google's display rules |
| `errors` | Required properties are missing or values are invalid; the affected rich results will not show |
| `none` | The page has no JSON-LD or Microdata (listed with **Pages in the results** = `problems+missing` or `all`) |
| `failed` | The page could not be read (not charged) |

The site summary (`SUMMARY` in the key-value store) has a machine-readable `status` (`SUCCESS`, `PARTIAL_RESULTS`, `FAILED`, `NO_RESULTS`, `LIMIT_REACHED`), counts per type, legacy rich result type (`richResults`) and issue code, FAQ display-status page counts (`faqGoogleDisplayStatusPages`), pages with Microdata (`withMicrodata`) and RDFa (`withRdfa`), the most frequent social tag issues, `pagesWithoutStructuredData`, `invalidInputs`, `duplicatesMerged`, `costPlan` and, when the run stopped at your limit, `notProcessed`. `SUMMARY.richResults` retains the legacy rule-check meaning above; it is not a sum of `markupEligibleRichResultCount`.

**Errors** are problems that stop a rich result. **Warnings** are recommended properties that make results richer. Paths like `$[0].offers` show where the problem is: `$[0]` is the first JSON-LD block on the page, `microdata[0]` the first Microdata item. A rich result with an error in a nested item (for example an invalid `offers.price`) is not eligible.

#### Issue codes

| code | Meaning |
|---|---|
| `REQUIRED_MISSING` / `RECOMMENDED_MISSING` | A required or recommended property is missing (`property` names it; `a\|b` means one of them) |
| `INVALID_DATE`, `INVALID_DURATION`, `INVALID_PRICE`, `INVALID_CURRENCY`, `INVALID_NUMBER`, `RATING_OUT_OF_RANGE` | Value in the wrong format, or a `ratingValue` outside `worstRating`-`bestRating` (default 1-5) |
| `INVALID_AVAILABILITY`, `INVALID_EVENT_STATUS`, `TIMEZONE_MISSING` | Value not in the schema.org list, or an event time without time zone |
| `RELATIVE_URL`, `EMPTY_VALUE` | URL property that is not absolute, or an empty value |
| `BREADCRUMB_NAME_MISSING`, `BREADCRUMB_URL_MISSING`, `BREADCRUMB_POSITION` | Breadcrumb item problems |
| `INVALID_JSON`, `MISSING_CONTEXT`, `INVALID_CONTEXT`, `MISSING_TYPE` | Broken JSON-LD blocks (including a block that is not an object) |
| `RICH_RESULT_LIMITED`, `RICH_RESULT_RETIRED` | Notes about FAQ and HowTo results (severity `info`) |

Social tag codes (in `socialIssues`): `OG_TITLE_MISSING`, `OG_DESCRIPTION_MISSING`, `OG_IMAGE_MISSING`, `OG_IMAGE_RELATIVE`, `TWITTER_CARD_MISSING` and `CANONICAL_MISSING` (the last two are `info`).

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/structured-data-validator) to Claude, Cursor or any MCP client, then ask e.g. "Check the structured data of these five product pages and tell me which ones are not eligible for product rich results."

```json
{ "urls": ["https://www.allbirds.com/products/mens-tree-runners"] }
```

Pages that cannot be read are not charged and carry an `errorType` (`blocked`, `not_found`, `robots_disallowed`, `unreachable`, `invalid_input`, `timeout`…).

### Use it from code and integrations

Run it from your own code with the Apify API. This call waits for the run and returns the results as JSON (replace `YOUR_TOKEN` with your Apify API token):

```bash
curl -X POST "https://api.apify.com/v2/acts/tidytools~structured-data-validator/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":["https://www.allbirds.com/products/mens-tree-runners","https://www.ikea.com/us/en/p/billy-bookcase-white-00263850/","https://stripe.com/","https://webscraper.io/test-sites/e-commerce/allinone/product/60"]}'
```

The synchronous endpoint waits up to 5 minutes. For bigger runs, start the run with `POST https://api.apify.com/v2/acts/tidytools~structured-data-validator/runs` and read the dataset when it finishes, or use the `apify-client` package for JavaScript or Python.

**Schedules and integrations:** run it daily or weekly with Apify Schedules, get a webhook when a run finishes, or send the results to Zapier, Make, n8n, Google Sheets, Slack and other apps with Apify integrations. Results can be exported as JSON, CSV, Excel or XML.

### Tips

- Use **Browser** mode if your markup is added by JavaScript or Google Tag Manager; the default reads the HTML that search engines fetch first.
- For large sites set **Pages in the results** to `problems+missing` (or `problems`); the summary still counts every page.
- For a full on-page SEO audit (titles, meta descriptions, H1, canonical, alt text), use **SEO Audit Crawler** (by TidyTools). To find broken links, use **Broken Link Checker** (by TidyTools).

### Advanced settings

- **Plain HTTP requests from**: *Auto* (recommended) reads pages from our servers and retries from Apify's network when a site blocks data-center requests, before falling back to a browser. Each result has `via` (`backend`, `direct` or `browser`).
- **Time limit per page**: default 120 seconds for reading a page, all retries and the browser fallback included. A slower page gets `errorType: "timeout"` and is not charged. 0 = no limit.
- **Proxy**: optional Apify Proxy for requests sent from Apify's network (billed to your Apify account).

### Limitations

- **JSON-LD** and **Microdata** are validated. **RDFa** is only detected: its types are listed in `rdfaTypes` (with a note), but its properties are not checked.
- The rules follow Google's documented required and recommended properties for common rich result types. Google alone decides whether a rich result appears, so "eligible" means "no blocking errors found", not a guarantee.
- Google shows FAQ rich results regularly only for well-known, authoritative government and health sites. Valid FAQ markup on another site is not invalid schema, but it does not mean Google will show an FAQ result; Google makes that decision. Check `faqGoogleDisplayStatus` rather than treating `eligible: true` or `richResults: ["FAQ"]` as display approval. Google no longer shows HowTo results; that markup is still validated and flagged with a note. See [Google Search Central's FAQ and HowTo update (August 8, updated September 14, 2023)](https://developers.google.com/search/blog/2023/08/howto-faq-changes).
- **robots.txt is respected** for every page the Actor reads, including the pages you list: the `TidyToolsFetch` group is used first, then `TidyTools`, then `*`. A disallowed page (or a page that a redirect lands on and robots.txt disallows) is not read and gets a free `robots_disallowed` row; `SUMMARY.skippedByRobotsTxt` counts them. In crawl and sitemap mode, disallowed pages are skipped and counted.
- A JSON-LD block wrapped in `<!-- -->` comments or `//<![CDATA[` markers is reported as `INVALID_JSON`, because the wrapper makes it invalid JSON; remove the wrapper.
- A review nested in a Recipe, Article, Event or similar without `reviewRating` is a warning (that review is not shown), not an error: the Recipe itself stays eligible. In a Product, the same problem is an error.

### Extract or validate? (checked September 2026)

Several Store Actors only **extract** JSON-LD. This one also **validates** it against Google's rich result rules:

| Actor | Price per 1,000 pages (free plan) | Validates against rich result rules |
|---|---|---|
| **Schema Markup Validator (this Actor)** | **$2** | Yes, with value checks and issue codes |
| logiover/json-ld-schema-meta-tag-extractor | $5 | No (extraction only) |
| pink_comic/schema-markup-extractor | $2 plus a start fee | No (extraction only) |
| autofacts/metadata-scraper | $5; the max charge per run must be set to at least $0.10 | Metadata extraction and SEO score |
| maximedupre/schema-markup-validator | **$0.05** | "Rich-result readiness signals"; reads JSON-LD, Microdata and RDFa; no value format checks mentioned |
| jungle_synthesizer/structured-data-validator-pro | $0.10 + $0.10 start fee per run (up to 15 URLs per run) | Required fields for 7 types |

maximedupre is much cheaper. Choose this Actor when you need Google's required and recommended properties for about 20 types, value format checks (dates, prices, currencies, availability), stable issue codes with JSON paths, sitemap input with URL patterns, `@graph`/`@id` handling and a real browser for pages that block plain requests or load almost empty until JavaScript runs (*Browser* mode renders every page, for markup added by Google Tag Manager).

### FAQ

**How many pages should I start with?** Start with 5 to 10 URLs (about $0.01 to $0.02): paste them in **URLs or domains** and click Start. When the results look right, use the sitemap option with an include pattern such as `https://example.com/products/**` to check one section of the site, or leave the pattern out to check every page.

**What is charged and what is free?** Each validated page costs $2 per 1,000 pages, with no start fee, and pages without structured data are charged too, because knowing a page has none is the result. Pages that cannot be read (blocked, 4xx/5xx, not HTML, timed out, disallowed by robots.txt), invalid input lines (including private or local addresses) and duplicate URLs are free. **Pages in the results** only filters the dataset; every checked page is still charged.

**How is it different from Google's Rich Results Test?** You can check a list of URLs, every page in your sitemap with **Validate the pages in the sitemap**, or a whole site with **Crawl the website**, instead of pasting URLs into a testing tool one by one. Every problem comes with a stable issue code, severity and path, ready to filter in a spreadsheet, and each result links to Google's Rich Results Test in `googleTestUrl`. Google alone decides whether a rich result appears, so eligible means no blocking errors found, not a guarantee.

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**Which structured data formats does it validate?** It extracts schema.org markup in JSON-LD and Microdata and checks it against what Google needs for rich results, such as Product, Article, Breadcrumb, Recipe, Event and JobPosting. Microdata is validated with the same rules unless you turn off **Also validate Microdata**. RDFa is only detected: its types are listed in `rdfaTypes`, but its properties are not checked.

**Can it check JSON-LD that is added by JavaScript or Google Tag Manager?** Yes. Set **Page fetching (HTTP or browser)** to *Browser* to always validate the JavaScript-rendered page. The default *Auto* mode reads the raw HTML that search engines see first and uses a real browser only when a site blocks plain requests or the page comes back almost empty (a JavaScript app shell).

**What happens if I give no valid URL?** The run does not fail: the dataset gets a free row with `errorType: "invalid_input"` explaining what is wrong, `SUMMARY.status` is `FAILED` and nothing is charged.

**Does it follow robots.txt?** Yes, for every page it reads, including the ones you list. It identifies itself as `TidyToolsFetch` and reads the matching robots.txt group (`TidyToolsFetch`, then `TidyTools`, then `*`). A disallowed page is not requested and gets a free row with `errorType: "robots_disallowed"`.

### Support

Open an issue in the **Issues** tab with an example URL. Issues are checked regularly.

Found this useful? A short review on the Store helps other users find it.

# Actor input Schema

## `urls` (type: `array`):

Main input (fill this, or `startUrls`). Pages to validate (product, article, recipe, event pages...), one per line: full URLs or bare domains. Up to 10,000 per run. Each page is requested exactly as given (only a #fragment is dropped) and keeps your line in inputUrl. With "Crawl the website" on, the crawl starts from these pages. Invalid lines become a row with errorType "invalid_input" (not charged).

## `startUrls` (type: `array`):

Older URL field, kept for existing API calls. Use it to upload or link a .txt/.csv file of page URLs. Full URLs only here: bare domains such as example.com go in "URLs or domains" above. Both fields are combined.

## `crawl` (type: `boolean`):

Follow internal links and validate every page found, up to Max pages. Off: only the listed pages are checked.

## `useSitemap` (type: `boolean`):

Read the site's XML sitemap (found automatically from robots.txt or /sitemap.xml) and validate every listed page, up to Max pages. Combine with Include URL patterns to check e.g. only /products/ pages.

## `sitemapUrl` (type: `string`):

Use this sitemap instead of finding one automatically. Setting it turns on sitemap validation.

## `maxPages` (type: `integer`):

Stop after this many pages. Used when crawling or validating a sitemap.

## `report` (type: `string`):

Which checked pages go to the dataset. Every page is still checked and charged, and the SUMMARY counts all of them (it also lists pages without structured data in pagesWithoutStructuredData). Values: all = All checked pages; problems+missing = Problems and pages without structured data; problems = Only pages with errors or warnings.

## `onlyProblems` (type: `boolean`):

Kept for existing API calls: true is the same as "Pages in the results" = "Problems and pages without structured data" (pages with no structured data are listed too). Ignored when "Pages in the results" is set.

## `includeJsonLd` (type: `boolean`):

Add the extracted JSON-LD objects to each result, so you can also use this as a structured data extractor.

## `validateMicrodata` (type: `boolean`):

Validate schema.org Microdata (itemscope/itemprop) with the same rules as JSON-LD. Its issues are marked format: "microdata". RDFa types are listed in rdfaTypes but not validated.

## `mode` (type: `string`):

"Auto" reads the raw HTML (what search engines see first) and uses a real browser only when a site blocks plain requests. "Browser" always validates the JavaScript-rendered page, for markup injected by JavaScript (e.g. Google Tag Manager). Values: auto = Auto (recommended); fast = Raw HTML only; browser = Browser (rendered page).

## `includeUrlPatterns` (type: `array`):

Optional glob patterns; only matching pages are validated when crawling or reading a sitemap, e.g. https://example.com/products/\*\*

## `excludeUrlPatterns` (type: `array`):

Optional glob patterns of pages to skip when crawling or reading a sitemap, e.g. **/tag/**

## `maxConcurrency` (type: `integer`):

How many pages are checked at the same time.

## `httpVia` (type: `string`):

Some sites block requests from data centers. Auto retries from a second network before falling back to a real browser. If both are refused, Auto also tries our second server (Oracle, different IP) before a real browser.

## `pageTimeoutSecs` (type: `integer`):

A page that takes longer to read (all retries and the browser fallback included) is reported with errorType "timeout" and not charged. 0 = no limit.

## `proxyConfiguration` (type: `object`):

Only used for requests sent from Apify's network. Apify proxy usage is billed to your Apify account.

## Actor input object example

```json
{
  "urls": [
    "https://www.allbirds.com/products/mens-tree-runners",
    "https://www.ikea.com/us/en/p/billy-bookcase-white-00263850/",
    "https://stripe.com/",
    "https://webscraper.io/test-sites/e-commerce/allinone/product/60"
  ],
  "crawl": false,
  "useSitemap": false,
  "maxPages": 100,
  "report": "all",
  "onlyProblems": false,
  "includeJsonLd": true,
  "validateMicrodata": true,
  "mode": "auto",
  "maxConcurrency": 10,
  "httpVia": "auto",
  "pageTimeoutSecs": 120
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `full` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://www.allbirds.com/products/mens-tree-runners",
        "https://www.ikea.com/us/en/p/billy-bookcase-white-00263850/",
        "https://stripe.com/",
        "https://webscraper.io/test-sites/e-commerce/allinone/product/60"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/structured-data-validator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": [
        "https://www.allbirds.com/products/mens-tree-runners",
        "https://www.ikea.com/us/en/p/billy-bookcase-white-00263850/",
        "https://stripe.com/",
        "https://webscraper.io/test-sites/e-commerce/allinone/product/60",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tidytools/structured-data-validator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://www.allbirds.com/products/mens-tree-runners",
    "https://www.ikea.com/us/en/p/billy-bookcase-white-00263850/",
    "https://stripe.com/",
    "https://webscraper.io/test-sites/e-commerce/allinone/product/60"
  ]
}' |
apify call tidytools/structured-data-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tidytools/structured-data-validator"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/H8eTuBhWOXKumBSIs/builds/JQpVq10a0hkeu0KJk/openapi.json
