# Backlink Monitor & Lost Link Alerts (`automa-flow/backlink-monitor`) Actor

Monitor backlinks you already earned or paid for. Detect removed links, dofollow-to-nofollow changes, anchor edits, redirects and noindex pages with structured before/after data.

- **URL**: https://apify.com/automa-flow/backlink-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** SEO tools, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 backlink checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Backlink Monitor & Lost Link Alerts

Monitor backlinks you already earned or paid for. This Actor re-checks a
watchlist of known placements and tells you when a link disappears, becomes
nofollow, changes anchor text, picks up `sponsored`/`ugc`, moves behind a
redirect, or lands on a `noindex` page.

The first published version is described in the [changelog](https://apify.com/automa-flow/backlink-monitor/changelog).

Supply the pages that should link to your site. The Actor checks those known
placements; it does not discover new backlinks or crawl a website.

### Try one placement

```json
{
  "monitorKey": "client-a",
  "placements": [
    {
      "id": "guest-post-001",
      "sourceUrl": "https://example.com/",
      "expectedTarget": "https://iana.org/domains/example",
      "expectedAnchor": "Learn more",
      "expectedRel": "dofollow",
      "label": "Public example.com IANA citation"
    }
  ],
  "mode": "changesOnly",
  "baselineMode": "emitBaseline",
  "confirmLossAfter": 2
}
```

The Console demo uses `mode: all`, returning `BASELINE` first and `NO_CHANGE`
on an unchanged repeat. The monitoring example above stores last-good state
and emits `BASELINE` on its first run. Schedule the same
input daily. Unchanged placements stay quiet unless `mode` is `all`.

### What changes it catches

| Code | Meaning |
| --- | --- |
| `BASELINE` | First successful observation for a placement |
| `LINK_LOST` | Link still missing after two-strike confirmation |
| `LINK_RESTORED` | A previously lost link is present again |
| `ANCHOR_CHANGED` | Visible anchor text changed |
| `TARGET_CHANGED` | Matched `href` changed |
| `TARGET_REDIRECT_CHANGED` | Target redirect destination changed |
| `DOFOLLOW_TO_NOFOLLOW` / `NOFOLLOW_TO_DOFOLLOW` | Effective follow signal flipped |
| `SPONSORED_ADDED` / `SPONSORED_REMOVED` | `rel=sponsored` appeared or left |
| `UGC_ADDED` / `UGC_REMOVED` | `rel=ugc` appeared or left |
| `PAGE_NOINDEXED` / `PAGE_REINDEXABLE` | Page robots/noindex flipped |
| `SOURCE_HTTP_STATUS_CHANGED` / `TARGET_HTTP_STATUS_CHANGED` | Observed HTTP status changed |

One observation can carry several codes. `SUSPECTED_LOSS` is a status, not a
confirmed loss.

### Example Tasks

Choose a saved example, replace its source and target URLs, and keep
`monitorKey` stable across scheduled runs:

- [Monitor paid backlinks for link loss](https://apify.com/automa-flow/backlink-monitor/examples/monitor-paid-backlinks-for-link-loss)
- [Track guest-post link and anchor changes](https://apify.com/automa-flow/backlink-monitor/examples/track-guest-post-dofollow-changes)
- [Check a client backlink portfolio](https://apify.com/automa-flow/backlink-monitor/examples/agency-backlink-watchlist)

### Example `LINK_LOST`

```json
{
  "recordType": "CHANGE",
  "status": "LINK_LOST",
  "placementId": "guest-post-001",
  "changeTypes": ["LINK_LOST"],
  "changes": {
    "linkFound": { "previous": true, "current": false },
    "matchedTarget": { "previous": "https://iana.org/domains/example", "current": null }
  },
  "lossConfirmationCount": 0
}
```

### Example `DOFOLLOW_TO_NOFOLLOW`

```json
{
  "recordType": "CHANGE",
  "status": "CHANGED",
  "placementId": "guest-post-001",
  "changeTypes": ["DOFOLLOW_TO_NOFOLLOW", "ANCHOR_CHANGED"],
  "changes": {
    "rel": { "previous": [], "current": ["nofollow"] },
    "anchorText": { "previous": "Learn more", "current": "example.com" }
  }
}
```

### Input watchlist

Each placement needs a stable `id`, `sourceUrl` and `expectedTarget`. Optional
`expectedAnchor` and `expectedRel` choose which link to watch when the page
has more than one match; `label` is copied onto every row. Duplicate ids fail the
run before any fetch. Maximum 5,000 placements per run. Target matching is
explicit: `normalized` (default), `exact`, or `domain`. Query parameters are
never dropped. `www` vs apex is significant in `normalized` and `exact`
mode; `domain` treats `www.example.com` and `example.com` as the same host
and does **not** treat `blog.example.com` as the same host.

### Scheduling and webhooks

1. Save the watchlist as an Actor Task with a stable `monitorKey`.
2. Schedule it daily or weekly.
3. Add a webhook on `SUCCEEDED` and filter Dataset rows where `status` is
   `CHANGED`, `LINK_LOST` or `SUSPECTED_LOSS`. A run that hits
   `BILLING_LIMIT_REACHED` finishes `FAILED` and still contains the rows
   already verified, so that status needs its own webhook if the cap can be reached.

The named Key-value store `backlink-monitor-state` holds last-good snapshots
and pending-loss counters. Dataset runs are the history.

### Pricing examples

The price is **$0.003** per successfully checked backlink plus the platform
`apify-actor-start` event ($0.005 per GB, minimum one event). Platform usage is
included. The start event is charged even when all source checks fail.

| Daily watchlist | Checks / month | `backlink-checked` |
| --- | --- | --- |
| 50 | 1,500 | $4.50 |
| 500 | 15,000 | $45 |
| 2,500 | 75,000 | $225 |

The monthly totals including 30 starts at 1024 MB are $4.65, $45.15 and $225.15.
One check costs $0.008; the 5,000-placement maximum costs $15.005 at 1024 MB.
At 4096 MB the start fee is $0.020, so the maximum is $15.020.

Retries, source failures, robots blocks, parser failures, invalid input and
duplicate ids are not billed. Unchanged successful checks are billed once. Before fetching, the Actor reserves enough budget for each selected placement.
Placements beyond that capacity receive `NOT_CHECKED` / `BUDGET_LIMIT`, incur no
check charge and keep their previous state. Capacity is conservative: a failed
selected check does not cause an extra placement to be fetched. The partial run
ends with `BILLING_LIMIT_REACHED`; verified output remains available. Increase
the limit or submit only the skipped placements in the next run.

### Two-strike loss confirmation

False lost-link alerts are expensive. Default `confirmLossAfter` is `2`:

1. Link present → last-good stored.
2. Next successful page check, link missing → `SUSPECTED_LOSS`. Last-good is
   **not** overwritten.
3. Still missing on the next successful check → `LINK_LOST`.
4. If the link returns before confirmation, the suspected loss is cleared.

A source HTTP **404 or 410** is a successful observation that the page is gone,
so it counts toward two-strike loss (the Dataset `sourceStatus` shows 404/410).
A brief CDN 404 can therefore confirm loss; raise `confirmLossAfter` if that
is too sensitive. HTTP 403, 429, timeouts and 5xx never count.

### Failure semantics

`LINK_NOT_FOUND` is not `SOURCE_FAILED`. A 403, 429, timeout, 5xx, CAPTCHA/
challenge page, parser failure, or HTML with **zero** `<a href>` links is
**never** `LINK_LOST`. The source page must be fetched and parsed with at
least one HTML link before absence can count as a loss observation. One bad
placement never kills the batch. If every placement independently fails, the
run still `SUCCEEDED` with per-item `SOURCE_FAILED` or `INVALID_INPUT` rows.

### Limitations

- HTTP HTML only. No JavaScript rendering, no CAPTCHA solving, no login.
- Does not discover unknown backlinks or crawl a site.
- Does not claim Google index status, PageRank, domain authority or SEO value.
- `follow`-like HTML is not a ranking guarantee.
- `crates.io`-style JavaScript marketplaces often return HTML with no `<a href>`.
  That is `SOURCE_FAILED` (`NO_HTML_LINKS`), not verified link loss and not a
  browser fallback.

### API / MCP usage

Direct MCP endpoint:

```text
https://mcp.apify.com?tools=automa-flow/backlink-monitor
```

Ask an agent to monitor a named watchlist of source/target pairs and return
only `LINK_LOST` and `DOFOLLOW_TO_NOFOLLOW` rows. Bound spend with
`maxTotalChargeUsd` (at least $0.008 for one check at 1024 MB).

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automa-flow/backlink-monitor').call({
  monitorKey: 'client-a',
  placements: [
    {
      id: 'guest-post-001',
      sourceUrl: 'https://example.com/',
      expectedTarget: 'https://iana.org/domains/example',
    },
  ],
});
```

Replace `automa-flow` with your account if you are running a private copy.

### Source and legal notes

Only user-supplied public HTTP(S) URLs on ports 80/443 are fetched. Private,
loopback, link-local, multicast, metadata and credentialed URLs are rejected,
including redirect hops. The Actor respects `robots.txt` when enabled. It
does not bypass authentication, paywalls, CAPTCHAs or access controls. Report
HTML facts; do not treat them as search-engine scoring.

# Changelog

This Actor's version history is a separate document: https://apify.com/automa-flow/backlink-monitor/changelog.md

# Actor input Schema

## `placements` (type: `array`):

1-5000 known placements. Each needs a stable id, the public source page URL, and the expected target URL. expectedAnchor and expectedRel choose among multiple matching links. label is echoed on every row.

## `monitorKey` (type: `string`):

Namespace for saved last-good state. Keep it stable for a scheduled watchlist; use a different key for an independent client.

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

changesOnly is for alert webhooks. all emits one row per placement per run, including unchanged checks.

## `baselineMode` (type: `string`):

How a placement seen for the first time is reported. The baseline is always stored. The demo uses all mode and emits BASELINE or NO\_CHANGE on repeat runs.

## `confirmLossAfter` (type: `integer`):

Consecutive successful page checks with the link missing required before LINK\_LOST. Source failures never count. Default 2 (two-strike).

## `checkTargetStatus` (type: `boolean`):

When the source page contains a matching link, fetch that link and record its HTTP status and redirect chain. A missing link does not trigger a target fetch. Target failures never overwrite last-good target fields and never count as link loss.

## `respectRobotsTxt` (type: `boolean`):

Fetch robots.txt once per origin and skip disallowed source paths. A robots transport failure is unknown, not allow-all, and is never LINK\_LOST.

## `targetMatch` (type: `string`):

normalized compares scheme, host case, default ports and path/query (fragments dropped). exact requires the resolved href to equal the expected URL. domain matches hostnames after stripping one leading www. — blog.example.com does not match example.com. Query strings are never dropped.

## Actor input object example

```json
{
  "placements": [
    {
      "id": "example-iana",
      "sourceUrl": "https://example.com/",
      "expectedTarget": "https://iana.org/domains/example",
      "expectedAnchor": "Learn more",
      "expectedRel": "dofollow",
      "label": "Public example.com IANA citation"
    }
  ],
  "monitorKey": "default",
  "mode": "all",
  "baselineMode": "emitBaseline",
  "confirmLossAfter": 2,
  "checkTargetStatus": true,
  "respectRobotsTxt": true,
  "targetMatch": "normalized"
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

## `checks` (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 = {
    "placements": [
        {
            "id": "example-iana",
            "sourceUrl": "https://example.com/",
            "expectedTarget": "https://iana.org/domains/example",
            "expectedAnchor": "Learn more",
            "expectedRel": "dofollow",
            "label": "Public example.com IANA citation"
        }
    ],
    "mode": "all"
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/backlink-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 = {
    "placements": [{
            "id": "example-iana",
            "sourceUrl": "https://example.com/",
            "expectedTarget": "https://iana.org/domains/example",
            "expectedAnchor": "Learn more",
            "expectedRel": "dofollow",
            "label": "Public example.com IANA citation",
        }],
    "mode": "all",
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/backlink-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 '{
  "placements": [
    {
      "id": "example-iana",
      "sourceUrl": "https://example.com/",
      "expectedTarget": "https://iana.org/domains/example",
      "expectedAnchor": "Learn more",
      "expectedRel": "dofollow",
      "label": "Public example.com IANA citation"
    }
  ],
  "mode": "all"
}' |
apify call automa-flow/backlink-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/backlink-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/3l7y9orLq4xf4L2cF/builds/bvldlooJ2SxrbS6df/openapi.json
