# SEO Audit & Change Monitor (`ozzie_bagadirov/seo-audit-change-monitor`) Actor

SEO audit for a whole website: broken links, redirects, duplicate titles and descriptions, sitemap and indexing problems. Get a health score and a fix for every issue, and see what changed since the last run. Up to 5,000 pages per run.

- **URL**: https://apify.com/ozzie\_bagadirov/seo-audit-change-monitor.md
- **Developed by:** [Ozzie Bagadirov](https://apify.com/ozzie_bagadirov) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / site audit, up to 250 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

Run a technical SEO audit of a website, up to 5,000 pages per run: broken links, redirects,
duplicate titles and descriptions, and sitemap and indexing problems. Each run gives you a
**health score**, a **list of issues sorted by how serious they are**, with how to fix each
one, and a row of data for **every page**. Run it again and it tells you what changed.

### What you get

The Output view of every run opens on the Report. The menu above it switches to the other
views. The Console shows the Report in a short window that does not scroll, so it shows the
score, the coverage and the run's notes; open the address above it in a new tab to read the
whole report.

| View | What is in it |
|---|---|
| **Report** | A readable summary: health score, errors, warnings and notices, what changed since the last run, and every issue with where it is and how to fix it. The header says how much of the site the run covered: every page found was checked, some pages found were left out, or stopped before checking every page found. |
| **Issues** | One row per problem, errors first. Each row has the URL, the exact value that is wrong, and how to fix it. |
| **Pages** | One row per page checked: status code, title, headings, word count, indexability, sitemap membership and more. |
| **Changes** | One row per thing that changed since the last run, with the value before and after. |

The Issues, Pages and Changes views can be downloaded as CSV, Excel or JSON, or read through
the Apify API. The menu also has **Summary (JSON)**: the run's totals, for automation. The
Issues and Changes links in a run's Output need your Apify API token when they are read
outside the Console, because Apify does not sign links to named datasets. The Report, Pages
and Summary links are signed.

### What it checks

28 checks, in the three levels most SEO tools use:

- **Errors** stop pages from working or from ranking. Fix these first.
- **Warnings** hurt how well pages rank or how they look in search results.
- **Notices** are worth knowing about but are often fine.

| Issue | Severity | What it means | `issue` value |
|---|---|---|---|
| Page did not respond | Error | The server did not answer at all. | `unreachable` |
| Redirect loop | Error | The URL keeps redirecting and never reaches a page. | `redirectLoop` |
| Server error (5xx) | Error | The page answered with a 5xx server error. | `serverError` |
| Page not found (4xx) | Error | The page answered with a 4xx error, usually 404. | `brokenPage` |
| Broken link | Error | A link on the page goes to a page that returns 404 or 410, or to a domain that no longer exists. Sites that only block automated checks, such as LinkedIn, are not counted. | `brokenOutlink` |
| Missing title | Error | The page has no `<title>`, or an empty one. | `titleMissing` |
| Duplicate title | Error | Another page on the site has exactly the same title. Pages set to noindex, or whose canonical tag points to another page, are not counted. | `duplicateTitle` |
| Duplicate content | Error | Another page on the site has exactly the same text. Pages set to noindex, or whose canonical tag points to another page, are not counted. | `duplicateContent` |
| Hreflang without return link | Error | The page names a language version that does not name it back. Pages that were not crawled are not counted. | `hreflangNotReciprocal` |
| Long redirect chain | Warning | More than two redirects before the page answers. | `redirectChainLong` |
| Page stopped loading | Warning | The page started to load and then stopped, so it could not be checked. | `incompleteResponse` |
| Page too large to check | Warning | The page is over about 1.6 MB, so its content was not checked and its links were not followed. | `pageTooLarge` |
| Missing meta description | Warning | The page has no meta description, or an empty one. | `metaDescriptionMissing` |
| Missing H1 | Warning | The page has no `<h1>` heading. | `h1Missing` |
| Missing canonical | Warning | The page has no canonical link tag. | `canonicalMissing` |
| Canonical points to another domain | Warning | The canonical tag points at a different domain, so search engines credit that domain instead. | `canonicalOffDomain` |
| Image without alt text | Warning | At least one image has no `alt` attribute. `alt=""` is correct for decorative images and is not counted. | `imagesMissingAlt` |
| Thin content | Warning | The page has fewer than 100 words of visible text. | `thinContent` |
| Orphan page | Warning | The page is in the sitemap, but no other page that was checked links to it. Only reported when every page the crawl found was checked. | `orphanPage` |
| Sitemap URL blocked by robots.txt | Warning | The sitemap lists this URL, but robots.txt tells crawlers not to fetch it. | `sitemapUrlBlocked` |
| Redirect | Notice | The URL redirects before the page answers. | `redirected` |
| Title too long | Notice | The title is over 60 characters, so it may be cut off in search results. | `titleTooLong` |
| Title too short | Notice | The title is under 15 characters. | `titleTooShort` |
| Meta description too long | Notice | The meta description is over 160 characters, so it may be cut off in search results. | `metaDescriptionTooLong` |
| More than one H1 | Notice | The page has more than one `<h1>` heading. | `h1Multiple` |
| Not indexable | Notice | The page tells search engines not to index it. In the `issues` column it reads `notIndexable:` followed by the reason: `meta-robots`, `x-robots-tag` or `canonicalised-away`. The directives that count are `noindex` and `none`, set for all crawlers or for Googlebot. | `notIndexable` |
| Duplicate meta description | Notice | Another page on the site has exactly the same meta description. Pages set to noindex, or whose canonical tag points to another page, are not counted. | `duplicateMetaDescription` |
| Page missing from sitemap | Notice | The page can be indexed but is not in the sitemap. Only reported when the site has a sitemap. Subdomains that sitemap lists no pages for are not checked. | `notInSitemap` |

Content checks only run on pages that loaded (status 2xx) and are HTML. A 404 page is
reported once, as a 404, and not again for its missing description.

A check the run could not make is listed under **Not checked this run**, never as passed. The
three duplicate checks, for example, compare only indexable pages, so a site that is entirely
noindex has nothing for them to compare.

The **health score** is the share of checked pages that have no errors, from 0 to 100.

### How to run a website audit

1. Enter your website address in **Website URL**, for example `https://www.example.com`.
2. Set **Max pages** if your site has more than 250 pages.
3. Click **Start**. A 250-page site usually takes a few minutes.
4. Read the **Report**. Click an issue to see which pages have it.
5. Fix the errors, then run it again on the same site to see what changed.

The default timeout is 60 minutes, and a 5,000-page audit needs most of it. With a shorter
timeout, up to half of it is kept for checking links to other sites and saving the results,
so a 5-minute run crawls for about two and a half minutes. With a timeout of about 4 minutes
or less, links to other sites are not checked, and the run log says so. With a timeout under
about 40 seconds there is not enough time left after the crawl for one slow page, and the run
log warns that the run could end before its results are saved.

For website monitoring, add a [schedule](https://docs.apify.com/platform/schedules) (weekly
is a good start) and connect an [integration](https://docs.apify.com/platform/integrations)
such as Slack or email, so you hear about changes and catch an SEO regression, like a new
404, at the next run. Every successful run ends with a one-line status, for example:
`Checked 250 pages. Health score 82/100. 3 changes found. Charged: audit-small.` A message
template can show it with `{{resource.statusMessage}}`. Integrations fire on every
successful run, whether or not anything changed; to be alerted only on changes, read
`changes.changesTotal` from `summary.json` in a webhook, Make or n8n step.

### Changes since the last run

The first run saves a snapshot of your site. Every later run on the same site compares
against the last run with the same URL patterns, link depth and sitemap setting, and lists
what changed. The first run with new settings compares against the site's last run and says
that what the crawl could reach has changed. The last eight snapshots are kept, plus the
last one for each of the five settings used most recently, in your own Apify storage.
`example.com` and `www.example.com` share one history store. Each spelling compares against
its own last crawl, so switching between them reports the change of address once and nothing
as removed.

The history is a key-value store named `wcm-…` in your Storage, and Apify keeps it until you
delete it. Delete it, or set a **History name**, to start the history again.

The Report lists the changes, and shows how the count for each issue moved since the last
run. It shows up to 50 changes, worst first: a page that became noindex, then a status that
changed, then canonical and redirect changes, then titles and meta descriptions, and URLs
newly found or no longer found last. Every kind of change the run found gets at least one
row, so a site-wide rewrite of one field cannot fill the table on its own. The Changes view
has every row: the URLs no longer found first, then every other change, each in URL order,
with `changeStatus`:

- **NEW**: the crawl found the URL this time and not last time
- **GONE**: the crawl found the URL last time and not this time. The page was removed, or
  nothing the crawl checked links to it any more; the row's `previousValue` is its last status
- **FIXED**: something was repaired, such as a page that works again
- **CHANGED**: anything else that differs

A page that starts returning 404 gives you one row, not a row for every field of the error
page.

If a run stops before checking every page it found (for example, at the page limit), pages
it did not reach are **not** reported as GONE. Newly found and no-longer-found URLs, and the
**Since last run** counts, also need runs that check every page they find: set **Max pages**
above the size of your site, or use **Only check URLs matching** to keep the crawl under
5,000 pages. Until then you still get every change to the pages the run did check. A run
whose maximum cost lowered **Max pages** below your site's size is a partial run in the same
way, so raise the maximum cost if you want NEW and GONE rows.

#### Every `changeType` you can filter on

| `changeType` | `changeStatus` | When |
|---|---|---|
| `url-added` | NEW | Found this time, not found by the previous crawl |
| `url-removed` | GONE | Found by the previous crawl, not found this time: removed, or no longer linked |
| `status-changed` | FIXED / CHANGED | FIXED when it went from 4xx/5xx back to 2xx |
| `title-changed` | CHANGED | The title text differs |
| `meta-description-changed` | CHANGED | The meta description differs |
| `canonical-changed` | CHANGED | The canonical URL differs |
| `canonical-off-domain` | CHANGED / FIXED | The canonical newly points to another domain, or stopped |
| `became-noindex` | CHANGED | The page went from indexable to not |
| `became-indexable` | FIXED | And back again |
| `redirect-chain-grew` | CHANGED | More redirects than last time |
| `redirect-chain-removed` | FIXED / CHANGED | Fewer redirects. FIXED when the URL now answers directly |
| `h1-count-changed` | CHANGED | The number of H1 headings differs |
| `images-missing-alt-changed` | FIXED / CHANGED | FIXED when fewer images lack alt text |

### How much does a site audit cost?

You pay once per audit. The price depends on how many pages were checked, not on how long
the run took. Apify platform usage is included.

| Pages checked | Price per audit |
|---|---|
| Up to 250 | $2.00 |
| 251 to 1,000 | $5.00 |
| 1,001 to 5,000 | $15.00 |

Paid Apify plans get a discount. Before the crawl starts, the run log shows the most this
run can cost.

- Runs that check fewer than 10 pages are free. A page counts as checked when your site
  answered it and did not refuse the crawler.
- You are never charged for more pages than you set in **Max pages**.
- A run that cannot reach your site is free, and so are the pages your site refuses. A page
  that answers 401, 403 or 429 was not read, so it is never charged for.
- If a run's maximum cost is below the price for your **Max pages**, the run checks fewer
  pages: at most 1,000 or 250, whichever the maximum cost covers, or 9 (free) if it covers
  neither. The run log says so before the crawl starts, and it is the first note on the
  Report.

### Input

Only **Website URL** is required. In the form, the last five fields are under **Crawl
settings** and **Change tracking**.

| Field | Default | What it does |
|---|---|---|
| Website URL | — | Where to start. A domain name, not an IP address. The crawl covers the whole domain, including its subdomains; on a hosted address such as `name.wordpress.com`, only that address. Public pages only. |
| Max pages | 250 | The most pages to check, from 10 to 5,000. |
| Check links to other sites | On | Checks up to 500 external links per run. |
| Use sitemap.xml | On | Also starts from the sitemap, and runs the three sitemap checks. |
| Max link depth | 10 | How many links deep to follow from the start page and from sitemap pages. |
| Only check URLs matching | — | Patterns matched against the full URL, such as `**/blog/**`. |
| Skip URLs matching | — | Patterns such as `**/tag/**` to leave parts out. |
| Requests per minute | 120 | Lower it if your site is slow. |
| History name | — | Usually left empty. Set a name to keep a separate change history for the same site, or to start a fresh one. |

When the URL patterns, the link depth limit, the crawl-trap filter or your robots.txt leave
pages out, the orphan page check is skipped: a page that was left out may be the one that
links to it. The Report says so. Links skipped only because of a session id, a tracking tag, or
`add-to-cart`, `replytocom`, `orderby` or `filter` in the address do not count.

Example input:

```json
{
    "startUrl": "https://www.example.com",
    "maxPages": 500
}
```

### Output

The **Issues** view, one row per problem:

```json
{
    "severity": "error",
    "issue": "brokenOutlink",
    "name": "Broken link",
    "category": "Links",
    "url": "https://www.example.com/blog/launch",
    "detail": "Links to https://partner.example.org/old-page — 404",
    "howToFix": "Update the link or remove it."
}
```

The **Pages** view, one row per page (shortened here):

```json
{
    "url": "https://www.example.com/blog/launch",
    "status": 200,
    "severity": "error",
    "issueCount": 2,
    "issues": ["metaDescriptionMissing", "brokenOutlink"],
    "title": "We have launched",
    "titleLength": 16,
    "metaDescription": null,
    "canonical": "https://www.example.com/blog/launch",
    "indexable": true,
    "h1Count": 1,
    "wordCount": 412,
    "imagesMissingAlt": 0,
    "inSitemap": true,
    "depth": 0,
    "responseTimeMs": 184,
    "outLinks": [
        {
            "url": "https://partner.example.org/old-page",
            "internal": false,
            "status": 404,
            "statusReason": null
        }
    ]
}
```

A page row also has the redirect chain, hreflang tags, and up to 500 links with their status.
`depth` counts links from the start page or, for pages found in the sitemap, from the sitemap.

The **Changes** view, one row per change:

```json
{
    "url": "https://www.example.com/pricing",
    "changeType": "title-changed",
    "changeStatus": "CHANGED",
    "field": "title",
    "previousValue": "Pricing",
    "currentValue": "Plans and pricing",
    "previousCrawledAt": "2026-09-09T08:00:00.000Z",
    "crawledAt": "2026-09-16T08:00:00.000Z"
}
```

Run totals for automation are in the `summary.json` record in the run's key-value store:
the health score, counts by severity and by issue, status codes, the change headline and
the price band charged. It also gives the ids of the run's three datasets, in
`datasets.pages`, `datasets.issues` and `datasets.changes`, and, when the maximum cost
lowered Max pages, `maxCostCap` with the pages asked for, the pages checked and the maximum
cost.

### Use it from the API

Apify's usual routes — `run-sync-get-dataset-items`, `runs/last/dataset` and
`defaultDatasetId` — return the Pages rows. The Issues and Changes rows are in datasets of
their own, and the run object gives their ids:

```js
const run = await client.run(runId).get();
const { items } = await client.dataset(run.storageIds.datasets.changes).listItems();
```

In Make or n8n, map `storageIds.datasets.changes` instead of `defaultDatasetId`.

### FAQ

#### Does it run JavaScript?

No. It reads the HTML your server sends. Pages that build their content in the browser
will look empty. In return, runs are fast and cheap.

#### Does it follow robots.txt?

Yes. Pages on your site that robots.txt disallows are not fetched. A `Crawl-delay` set for
all crawlers in the robots.txt of the address you enter is followed, down to one request a
minute. If that robots.txt answers with a server error or cannot be read, the run log and
the Report both say so, and the crawl goes ahead as if it allowed everything. Search engines
read a 5xx robots.txt as "do not crawl this site", so it is worth fixing.

#### What counts as my site?

The whole domain you enter, including its subdomains. Starting at `blog.example.com` also
covers `example.com` and `shop.example.com`. Use **Only check URLs matching** to stay in one
section. On a hosted address such as `name.wordpress.com` or `name.substack.com`, only that
address is checked; links to the platform and to other sites on it are checked as links to
other sites. If the address you enter redirects to another domain, that domain is checked
instead, and the run log says so.

It has to be a domain name. An IP address, `localhost`, or any host without a dot is refused
before anything is crawled, and so is an address that carries a username or password. The
Input form cannot check that for you, so the run stops at once and says which it was.

#### Can it check pages behind a login?

No. Public pages only: the Actor cannot sign in. Do not put a secret token in the address:
it is saved with the results and in the change history.

#### A link works for me. Why does its row say 403?

Some sites, such as LinkedIn and Stack Overflow, refuse automated requests but work fine
in a browser. Those links keep their real status with `blocked-by-target` as the reason,
and they are **not** reported as broken. Only 404, 410 and domains that no longer exist
count as broken links.

#### Why did the crawl stop before my page limit?

A run stops early rather than fail when it is about to run out of memory or time. You get
everything it found, the Report says why it stopped, and the price is set by the pages it
checked. Very large pages and pages with several hundred links use the most memory. A run
gets 1 GB by default and at most 2 GB: set **Memory** to 2 GB in the run options to go
further. A run that already had 2 GB can check the rest of the site in parts with **Only
check URLs matching** or **Skip URLs matching**. A run stopped for time can be run again
with a longer timeout.

#### What does it look like in my server logs?

Like an ordinary desktop browser: it sends a desktop browser's own headers — Chrome's,
Edge's, Safari's or Firefox's — from Apify's cloud servers, whose addresses change. There is
no crawler name or fixed IP address to allow, and only the robots.txt rules for all crawlers
(`User-agent: *`) apply. If your site's bot protection blocks the run, lower **Requests per
minute**, or relax the protection while the run is going. Pages the protection refuses are
not charged for, so a run it blocks costs nothing unless the run still read 10 pages or more.

#### What happens if I abort a run?

Results are saved only after the crawl and the link check have finished. If you abort a run
before then, nothing is saved and you are not charged. For a shorter audit, lower **Max
pages** instead.

#### What happens if a run is restarted?

If Apify moves a run to another server during the crawl, the crawl starts again from the
beginning and you are charged once. If you resurrect a run that already completed its audit,
it stops at once, because its results are already in the run's storage. Start a new run to
check the site again.

#### Does my data go anywhere?

No. Results and history stay in your Apify account. The Actor only sends requests to your
site, to wherever your own redirects lead, and to the external links your pages contain:
up to 500 links a run, one or two requests each plus any redirects, and at most 2 requests
a second to any one host, each redirect step included. You can turn the external link check
off.

#### How do I get help?

Open an issue in the Issues tab on this Actor's page in Apify Store (not the Issues view of
a run). Support is in writing only. Issues there are public, so share the run from the issue
form rather than pasting private addresses or data.

### Limits

- Up to 5,000 pages per run.
- HTML pages only. PDFs and images are listed with their status but not read.
- Up to 500 links per page are recorded and checked.
- Pages over about 1.6 MB are listed with their status but not read.
- A redirect chain lists each URL, but not the status code of each step.
- SSL certificates are not checked. A page with an expired or wrong certificate is checked
  like any other page. A link to another site that fails its certificate check is listed
  with the reason `external-check-tls` and is not counted as broken.
- Sitemaps are read up to 50,000 URLs and 50 sitemap files. Past that, the check for pages
  missing from the sitemap is skipped, and the Report says so.
- If the site's pages link to more than 100,000 different URLs in total, the orphan check
  is skipped, and the Report says so.

# Changelog

This Actor's version history is a separate document: https://apify.com/ozzie\_bagadirov/seo-audit-change-monitor/changelog.md

# Actor input Schema

## `startUrl` (type: `string`):

The page to start from, with https://, not an IP address. Public pages only: the Actor cannot sign in. It covers the whole domain and its subdomains, or name.wordpress.com alone.

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

The most pages to check. You pay for the pages actually checked, in three bands: up to 250, up to 1,000 and up to 5,000.

## `checkExternalLinks` (type: `boolean`):

Also check links to other sites, up to 500 per run. Only 404, 410 and dead domains count as broken; sites that block robots, like LinkedIn, are not.

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

Also start from the sitemap, and report pages missing from it, orphan pages, and sitemap URLs blocked by robots.txt.

## `maxDepth` (type: `integer`):

How many links deep to follow from the start page and from sitemap pages.

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

Only check URLs that match, for example **/blog/**. Patterns are matched against the full URL. Leaving pages out turns off the orphan page check.

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

Skip URLs that match, for example **/tag/**. Leaving pages out turns off the orphan page check.

## `maxRequestsPerMinute` (type: `integer`):

Lower this if the site is slow or starts refusing requests.

## `historyKey` (type: `string`):

Usually left empty. Set a name to keep a separate change history for the same site, or to start a fresh one. Lowercase letters, digits and hyphens, up to 40.

## Actor input object example

```json
{
  "startUrl": "https://example.com",
  "maxPages": 250,
  "checkExternalLinks": true,
  "useSitemap": true,
  "maxDepth": 10,
  "includeUrlPatterns": [],
  "excludeUrlPatterns": [],
  "maxRequestsPerMinute": 120,
  "historyKey": ""
}
```

# Actor output Schema

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

Health score, issue counts by severity, what changed since the last run, and every issue type with where it is and how to fix it.

## `issues` (type: `string`):

One row per problem found, errors first: severity, issue, URL, the specific value that is wrong, and how to fix it. Outside the Console this link needs your Apify API token, unless your account lets anyone with the storage id read it.

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

One row per crawled URL, in overview columns: status, worst severity, issue count, title, H1 count, word count, indexability, sitemap membership and depth. Open a row in the Console, or drop ?view=overview from this link, for every field the audit recorded.

## `changes` (type: `string`):

One row per thing that changed since the previous run on the same site, with the value before and after. Empty on the first run. Outside the Console this link needs your Apify API token, unless your account lets anyone with the storage id read it.

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

Run totals for automation: health score, counts by severity and by issue, status counts, the change headline, and the billing band charged.

# 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 = {
    "startUrl": "https://example.com"
};

// Run the Actor and wait for it to finish
const run = await client.actor("ozzie_bagadirov/seo-audit-change-monitor").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 = { "startUrl": "https://example.com" }

# Run the Actor and wait for it to finish
run = client.actor("ozzie_bagadirov/seo-audit-change-monitor").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 '{
  "startUrl": "https://example.com"
}' |
apify call ozzie_bagadirov/seo-audit-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ozzie_bagadirov/seo-audit-change-monitor"
        }
    }
}
```

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/6TnMfavPnAFilTCcp/builds/VLD573fqhScgitMbO/openapi.json
