# Schema Markup Validator & Generator – JSON-LD Checker (`locomint/schema-markup-validator`) Actor

Schema markup validator for lists of pages: finds the schema.org JSON-LD on each URL and reports, type by type, the required fields that are missing and the recommended ones. Can also write suggested Organization or LocalBusiness markup for each page.

- **URL**: https://apify.com/locomint/schema-markup-validator.md
- **Developed by:** [Locomint](https://apify.com/locomint) (community)
- **Categories:** SEO tools, Developer tools, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 1,000 result delivereds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Schema Markup Validator & Generator – JSON-LD Checker

This schema markup validator reads a list of page URLs and reports, for every schema.org
JSON-LD object on each page, which required fields are missing (errors) and which recommended
ones are missing (warnings). It can also write suggested Organization or LocalBusiness markup
for each page from what the page says about itself, and check that too. $0.50 per 1,000 URLs.

### What it does

For every URL you give it, the actor checks the site's robots.txt, fetches the page over plain
HTTP (no browser), finds each `<script type="application/ld+json">` block and walks every
object in it, including `@graph` lists and nested objects. An Article's `publisher` is held to
the Organization rules just as a top-level Organization is. Each page produces one dataset row.

**What the validator reports**

| Field | What goes in it |
|---|---|
| `has_markup`, `blocks` | Whether the page has JSON-LD, and how many blocks. |
| `types` | Every `@type` declared on the page. |
| `valid` | `true` when at least one object was checked and nothing is wrong; `false` when there is any error; `null` when nothing on the page has a ruleset. |
| `checked_types`, `unchecked_types` | The rulesets that were applied, and the declared types no ruleset covers. |
| `errors` | Missing required fields (`WebSite: missing required name`), a block that is not valid JSON, a block with no `@context`, or nesting deeper than 100 levels. |
| `warnings` | Missing recommended fields (`Organization: missing recommended contactPoint, description`), one line per object, and a line when the page has no JSON-LD at all. |
| `items` | Up to 50 objects, each with its type, the ruleset it was checked against, `valid`, `missing_required`, `missing_recommended` and its `name`. |

**Which types are checked**

| Ruleset | Required | Recommended |
|---|---|---|
| Organization | name, url | logo, sameAs, contactPoint, description |
| LocalBusiness | name, address | telephone, openingHours (or openingHoursSpecification), geo, url, image, priceRange |
| WebSite | name, url | potentialAction |
| Product | name | image, description, brand, offers, aggregateRating, review |
| Offer | price, priceCurrency | availability, url, priceValidUntil |
| Article | headline | image, datePublished, dateModified, author |
| NewsArticle, BlogPosting | headline | image, datePublished, author |
| Event | name, startDate, location | endDate, offers, image, description |
| FAQPage / Question | mainEntity / name, acceptedAnswer | |
| BreadcrumbList | itemListElement | |

Also checked: JobPosting, Recipe, Review, AggregateRating, SoftwareApplication, VideoObject,
Person and PostalAddress. Twenty-two LocalBusiness subtypes (Restaurant, Dentist, Hotel,
CafeOrCoffeeShop, Plumber, LegalService and others) are judged by the LocalBusiness rules, and
31 Organization subtypes (Corporation, NGO, OnlineStore, NewsMediaOrganization,
EducationalOrganization and others) by the Organization rules.

A type with no ruleset, such as `SearchAction`, `ImageObject` or `WebPage`, comes back
**unchecked**, with `valid: null`, never as invalid. Correct markup is not accused of being
wrong because we hold no opinion on it.

**What the generator writes**

With `generateMarkup` set to `missing` (pages with no JSON-LD) or `always` (every page), the row
also carries a suggestion built from the page: `name` from the page title, `url`, `description`
from the meta or Open Graph description, a `contactPoint` with the first role email address on
the page, and `sameAs` with its social profiles. The suggestion is run back through the
validator:

| Field | What goes in it |
|---|---|
| `generated_jsonld` | The suggested object. |
| `generated_script` | The same object inside a `<script type="application/ld+json">` tag, with `<`, `>` and `&` escaped so a name containing `</script>` cannot break your page. |
| `generated_valid` | The validator's verdict on the suggestion. |
| `generated_missing` | Required and recommended fields the suggestion still lacks, for you to fill in. |

### Who it is for

- **SEO agencies and in-house SEO** auditing structured data across hundreds or thousands of
  pages at once, with errors and warnings as columns rather than one URL at a time in a browser.
- **Developers shipping structured data**, as a check in a release routine: run the key templates
  after a deploy and look for `valid: false`.
- **Site migrations**, where markup is often lost when templates change. Run the old and new
  URLs and compare `types` and `errors` row by row.
- **Small-business sites with no markup at all**, where `generateMarkup: missing` gives a
  starting point to complete and publish.

### How to use it

In the Apify Console:

1. Paste full URLs, including `https://`, into **Page URLs**, up to 5,000. The input form rejects
   bare domains such as `example.com`.
2. Leave **Write suggested markup** on "No, only validate", or choose "Only for pages with no
   JSON-LD" or "For every page". Pick the **Type for suggested markup**.
3. Set a maximum cost per run if you want a ceiling. The actor stops before the row that would
   pass it.
4. Start the run. The **Pages** view shows URL, status, whether the page has markup, its types,
   `valid`, errors and warnings. Export as CSV, Excel or JSON.

From the API, this call starts a run, waits for it and returns the rows:

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/locomint~schema-markup-validator/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"startUrls": [{"url": "https://www.python.org/"}], "generateMarkup": "missing"}'
```

The synchronous endpoint waits up to 300 seconds. For a long list, start the run with
`POST https://api.apify.com/v2/acts/locomint~schema-markup-validator/runs?token=YOUR_APIFY_TOKEN`
and read its dataset when the run finishes. With the Python client (`pip install
apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("locomint/schema-markup-validator").call(
    run_input={"startUrls": [{"url": "https://coffeecollective.dk/"}], "generateMarkup": "always"}
)
for row in client.dataset(run.default_dataset_id).iterate_items():
    print(row["url"], row["valid"], row["errors"], row.get("generated_missing"))
```

### Input example

```json
{
  "startUrls": [
    { "url": "https://coffeecollective.dk/" },
    { "url": "https://www.monmouthcoffee.co.uk/" }
  ],
  "generateMarkup": "always",
  "markupType": "Organization",
  "respectRobots": true,
  "concurrency": 5,
  "pageTimeout": 25
}
```

| Field | Default | Allowed | What it does |
|---|---|---|---|
| `startUrls` | required | 1 to 5,000 URLs | The pages to check. Exact duplicates are dropped. Through the API, plain strings work as well as `{ "url": ... }` objects. |
| `generateMarkup` | `off` | `off`, `missing`, `always` | Write suggested JSON-LD never, only for pages with none, or for every page. |
| `markupType` | `Organization` | Organization, LocalBusiness, Corporation, OnlineStore, ProfessionalService, Store, Restaurant, Dentist | The `@type` the suggestion is written as. |
| `respectRobots` | `true` | true / false | Skip a page that robots.txt disallows; the row arrives as `robots_disallowed`. |
| `concurrency` | 5 | 1 to 10 | Pages fetched at the same time. |
| `pageTimeout` | 25 | 5 to 60 seconds | A page with no answer by then is delivered as `unreachable`. |

### Output example

A real row, from a run on 11 September 2026 with the input above. `generated_script` is left
out here; it holds the same object as `generated_jsonld` inside a script tag.

```json
{
  "url": "https://coffeecollective.dk/",
  "final_url": "https://coffeecollective.dk/",
  "status": "ok",
  "http_status": 200,
  "has_markup": true,
  "blocks": 2,
  "types": ["Organization", "SearchAction", "WebSite"],
  "valid": true,
  "checked": true,
  "checked_types": ["Organization", "WebSite"],
  "unchecked_types": ["SearchAction"],
  "errors": [],
  "warnings": ["Organization: missing recommended contactPoint, description"],
  "items": [
    {"type": "Organization", "types": ["Organization"], "checked": true, "checked_against": "Organization", "valid": true, "missing_required": [], "missing_recommended": ["contactPoint", "description"], "name": "Coffee Collective"},
    {"type": "WebSite", "types": ["WebSite"], "checked": true, "checked_against": "WebSite", "valid": true, "missing_required": [], "missing_recommended": [], "name": "Coffee Collective"},
    {"type": "SearchAction", "types": ["SearchAction"], "checked": false, "checked_against": null, "valid": null, "missing_required": [], "missing_recommended": [], "name": null}
  ],
  "note": null,
  "checked_at": "2026-09-11T18:43:41+00:00",
  "generated_jsonld": {
    "@context": "https://schema.org",
    "@type": "Organization",
    "name": "Coffee Collective",
    "url": "https://coffeecollective.dk/",
    "description": "A specialty coffee roastery. We work directly with farmers to ensure quality, transparency, and sustainability - creating experiences from farmer to guest.",
    "sameAs": [
      "https://www.instagram.com/coffeecollectif",
      "https://www.linkedin.com/company/coffeecollective"
    ]
  },
  "generated_valid": true,
  "generated_missing": ["logo", "contactPoint"]
}
```

The suggestion adds the description the existing markup lacks. It has no `contactPoint`
because no role address appears in the homepage HTML, so `generated_missing` asks for one. In
an earlier run, python.org's `WebSite` object declared no `name`, so that page came back
`valid: false` with the error `WebSite: missing required name`.

#### Status values

| `status` | Meaning |
|---|---|
| `ok` | Fetched and parsed. `has_markup` is `false` when the page carries no JSON-LD. |
| `robots_disallowed` | robots.txt disallows the path, so the page was not fetched; `note` quotes the rule. |
| `refused` | The URL points at a private or internal network address. |
| `blocked` | The site answered with 403, 429 or a challenge page. Not retried. |
| `unreachable` | No connection, a DNS failure, or no answer within `pageTimeout`. |
| `http_error` | Any other 4xx or 5xx answer; `http_status` has the code. |
| `not_html` | The URL is a PDF, image or other non-text file. |

### Pricing

| Event | Price |
|---|---|
| Page checked (one dataset row) | $0.0005, which is $0.50 per 1,000 |
| Actor start | $0.00005 per GB of run memory, charged once per run |

Worked example: auditing 2,000 pages of a site costs 2,000 x $0.0005 = $1.00, plus $0.00005 for
starting a 1 GB run. 10,000 pages is two runs of 5,000 and $5.00. Apify's free plan gives $5 of
usage a month without a card, which covers just under 10,000 pages.

You pay only these event prices; Apify compute is not billed to you separately, and generating
markup costs nothing extra. Every URL produces one row and one charge, including `blocked`,
`unreachable` and `robots_disallowed`, because each is an answer about that URL. Entries that
are not URLs are skipped, named in the run log and not charged. If you set a maximum cost per
run, the actor stops before the row that would pass it.

### FAQ

#### Does it read microdata or RDFa?

No, JSON-LD only. A page whose structured data is written as microdata or RDFa comes back with
`has_markup: false` and a warning that says those formats are not read.

#### Why is a type unchecked instead of valid or invalid?

We hold rules for 20 types and their listed subtypes. Anything else, such as `SearchAction` or
`ImageObject`, is reported under `unchecked_types` with `valid: null`, because calling correct
markup invalid is worse than saying nothing.

#### What is the difference between an error and a warning?

An error is a missing required field or a block that cannot be read as schema.org, and it makes
`valid` false. A warning is a missing recommended field; the object still passes. These rules
say what a complete object needs; passing them does not guarantee that a search engine shows a
rich result.

#### Why is valid null on a page that has markup?

Because every object on it was of a type we do not check, so nothing was judged. `null` means
"no opinion", never a clean pass.

#### Should I publish the generated markup as it is?

Review it first. `name` is the page title as written, so a title such as "Home - Monmouth Coffee
Company" (from our own test run) needs editing, and the generator never writes an address,
phone number, opening hours or logo. With a LocalBusiness-family type the suggestion is not
valid until you add `address`, which is required.

#### Does it see markup added by JavaScript?

No. It reads the HTML the server sends. JSON-LD that a tag manager or a JavaScript framework
inserts after the page loads is not in that HTML and is not seen.

#### Does it honour robots.txt, and what happens when a site blocks it?

It reads each site's robots.txt once per run and applies the rules for all crawlers (the `*`
group); a disallowed page comes back `robots_disallowed`. A 403, 429 or challenge page is
reported as `blocked` and never retried from the same address or any other, and the host is
left alone for 15 minutes.

### Limits

- JSON-LD only; no microdata or RDFa. No JavaScript rendering.
- One page per URL; the actor does not crawl. 5,000 URLs per run, 25 seconds per page by default
  (5 to 60), 3 MB of HTML per page, at most 5 redirects.
- Rules check that a field is present, not what it holds: a `telephone` of "n/a" counts as
  present, and dates, URLs and prices are not validated.
- Subtypes outside the listed ones, such as `ClothingStore` or `AccountingService`, come back
  unchecked.
- `items` lists the first 50 objects on a page; `types`, `errors` and `warnings` cover all of
  them. Warnings repeat per object, so four Organization objects give four lines.
- The generator reads only the page itself: role email addresses on other pages are not used,
  and mailboxes that name a person are never written into a suggestion.

### Other Locomint actors

- [Google Maps Scraper & Email Extractor – Business Leads](https://apify.com/locomint/google-maps-scraper): Search terms and a city in, business records with website contacts out.
- [Google Maps Scraper – Multi-City Lead Lists with Emails](https://apify.com/locomint/google-maps-multi-city-scraper): Many categories across many cities in one deduplicated run.
- [Google Maps Place Details Scraper – Bulk Place ID Lookup](https://apify.com/locomint/google-maps-place-details-scraper): Place IDs or place-page links in, full records out.
- [Website Email Scraper – Contact Details, Socials & WhatsApp](https://apify.com/locomint/website-content-contact-extractor): Contact points from website URLs you supply.
- [Website Content Crawler – Markdown for AI, Emails & Contacts](https://apify.com/locomint/website-crawler-content-contacts): A whole site as Markdown, with its contact points.
- [Bulk Email Verifier & Validator](https://apify.com/locomint/bulk-email-verifier): Checks whether addresses can receive mail.
- [Company Enrichment API – Domain to Emails, Socials & Tech](https://apify.com/locomint/company-enrichment): A domain in, its contacts and technologies out.
- [AI Crawler Checker – robots.txt Rules for GPTBot & ClaudeBot](https://apify.com/locomint/ai-crawler-checker): Which AI crawlers a site's robots.txt allows.

# Actor input Schema

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

The pages to check. Full URLs including https://, one result per URL, up to 5,000 per run.

## `generateMarkup` (type: `string`):

Also write JSON-LD for the page from what it says about itself: its title, meta description, a role email address and its social profiles. The suggestion is run back through the validator. Review it before you publish it.

## `markupType` (type: `string`):

The schema.org type the suggestion is written as.

## `respectRobots` (type: `boolean`):

Skip a page that the site's robots.txt disallows. The row is still delivered with status robots\_disallowed.

## `concurrency` (type: `integer`):

How many pages to fetch at the same time.

## `pageTimeout` (type: `integer`):

A page that has not answered by then is delivered with status unreachable.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.nytimes.com/"
    },
    {
      "url": "https://www.python.org/"
    },
    {
      "url": "https://basecamp.com/"
    }
  ],
  "generateMarkup": "off",
  "markupType": "Organization",
  "respectRobots": true,
  "concurrency": 5,
  "pageTimeout": 25
}
```

# Actor output Schema

## `pages` (type: `string`):

One row per URL: the schema.org JSON-LD types found, valid/checked, errors (missing required fields) and warnings (missing recommended fields) per type, and the suggested markup when generation is on.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.nytimes.com/"
        },
        {
            "url": "https://www.python.org/"
        },
        {
            "url": "https://basecamp.com/"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("locomint/schema-markup-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 = { "startUrls": [
        { "url": "https://www.nytimes.com/" },
        { "url": "https://www.python.org/" },
        { "url": "https://basecamp.com/" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("locomint/schema-markup-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 '{
  "startUrls": [
    {
      "url": "https://www.nytimes.com/"
    },
    {
      "url": "https://www.python.org/"
    },
    {
      "url": "https://basecamp.com/"
    }
  ]
}' |
apify call locomint/schema-markup-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,locomint/schema-markup-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/GXk2Tjh3VfIQoftPd/builds/BaH4eHGJqKlU2fBh3/openapi.json
