# UTM Campaign URL QA Auditor (`automation-lab/utm-campaign-url-qa-auditor`) Actor

Audit campaign URLs in bulk for redirect chains, UTM preservation, HTTPS, final host/path rules, status codes, and timings.

- **URL**: https://apify.com/automation-lab/utm-campaign-url-qa-auditor.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use 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

## UTM Campaign URL QA Auditor

Audit marketing campaign URLs before launch. This Apify Actor follows every redirect, checks final landing pages, verifies UTM parameters, and returns one QA row per URL.

Use it for email links, paid social ads, influencer links, QR-code destinations, affiliate links, link shorteners, and media-plan QA spreadsheets.

### What does UTM Campaign URL QA Auditor do?

The actor accepts campaign URLs and validates that they still land where your team expects.

It checks:

- ✅ redirect chains and hop counts
- ✅ final status codes
- ✅ final URL, host, and path
- ✅ HTTPS landing-page requirement
- ✅ expected `utm_source`, `utm_medium`, `utm_campaign`, and other UTM parameters
- ✅ missing, changed, or unexpected UTM parameters
- ✅ response time per URL
- ✅ structured errors for timeout, DNS, blocked HEAD requests, and invalid URLs

### Who is it for?

#### Marketing operations teams

Use it before a campaign goes live to catch stripped UTMs, broken short links, wrong landing domains, and slow redirect chains.

#### Paid media agencies

Validate hundreds of tracking links from media plans before handing campaigns to clients or ad platforms.

#### Email and lifecycle teams

Check newsletter, CRM, and automation links after UTM tagging changes or landing-page migrations.

#### SEO and analytics teams

Find campaign attribution breaks caused by redirects, canonical changes, or accidental query-parameter rewrites.

### Why use this actor?

Manual link QA is slow and easy to miss. A single broken redirect or stripped `utm_campaign` can ruin attribution for an entire launch.

This actor gives you repeatable, exportable checks that fit into Apify schedules, webhooks, Make, Zapier, Airflow, and internal QA pipelines.

### What data can it extract?

| Field | Description |
| --- | --- |
| `inputUrl` | URL submitted by the user |
| `normalizedUrl` | URL after scheme normalization |
| `finalUrl` | Last URL reached after redirects |
| `finalStatus` | Last HTTP status code |
| `ok` | Overall pass/fail boolean |
| `hopCount` | Number of redirect hops |
| `totalMs` | Total time spent checking the URL |
| `httpsOk` | Whether final URL satisfies HTTPS requirement |
| `finalHost` | Host of the final landing URL |
| `finalHostMatchesExpected` | Whether final host matched your rule |
| `finalPathMatchesExpected` | Whether final path matched your rule |
| `missingParams` | Expected UTM keys that disappeared |
| `changedParams` | Expected UTM values that changed |
| `extraParams` | Unexpected UTM keys found on final URL |
| `utmOk` | Whether required UTM parameters passed |
| `redirectChain` | Per-hop status, location, timing, and content type |
| `canonicalUrl` | Optional HTML canonical URL |
| `metaRobots` | Optional robots meta tag |
| `error` | Row-level error if the URL could not be checked |

### How much does it cost to audit campaign URL UTMs?

The actor uses pay-per-event pricing: a small start event plus one event for each URL result. Because it is HTTP-only and defaults to HEAD requests with GET fallback, it is designed for cheap bulk QA.

For current live pricing, open the Pricing tab on the Apify Store page.

### Quick start

1. Add campaign URLs to **Campaign URLs**.
2. Add expected UTM parameters such as `utm_source` and `utm_medium`.
3. Optionally add an expected final host such as `example.com`.
4. Keep **Require HTTPS final URL** enabled for production campaigns.
5. Run the actor.
6. Export the dataset to CSV, JSON, Excel, or connect it to your workflow.

### Input: campaign URLs

You can paste URLs into the Apify request-list editor.

Example:

```json
{
  "startUrls": [
    { "url": "https://httpbin.org/redirect-to?url=https%3A%2F%2Fexample.com%2F%3Futm_source%3Dnewsletter%26utm_medium%3Demail" },
    { "url": "https://example.com/?utm_source=newsletter&utm_medium=email" }
  ],
  "expectedUtmParams": {
    "utm_source": "newsletter",
    "utm_medium": "email"
  },
  "expectedFinalHost": "example.com",
  "requireHttps": true
}
````

### Input: CSV media-plan rows

Use `csvText` when you already have a spreadsheet. Supported simple columns include:

- `url`
- `expectedFinalHost`
- `expectedFinalPath`
- UTM columns such as `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, and `utm_term`

Example:

```csv
url,expectedFinalHost,utm_source,utm_medium
https://example.com/?utm_source=newsletter&utm_medium=email,example.com,newsletter,email
```

### Per-URL expected rules

Each `startUrls` entry can override global rules:

```json
{
  "url": "https://short.example/spring-sale",
  "expectedFinalHost": "www.example.com",
  "expectedFinalPath": "/spring-sale",
  "expectedUtmParams": {
    "utm_campaign": "spring_sale"
  }
}
```

### Output examples

A passing result looks like this:

```json
{
  "inputUrl": "https://example.com/?utm_source=newsletter&utm_medium=email",
  "finalUrl": "https://example.com/?utm_source=newsletter&utm_medium=email",
  "finalStatus": 200,
  "ok": true,
  "hopCount": 0,
  "utmOk": true,
  "missingParams": [],
  "changedParams": [],
  "extraParams": []
}
```

A failed result might show:

```json
{
  "ok": false,
  "missingParams": ["utm_campaign"],
  "finalHostMatchesExpected": false,
  "error": null
}
```

### Redirect-chain diagnostics

The `redirectChain` array records each hop with:

- hop index
- URL requested
- method used (`HEAD` or `GET`)
- status code
- raw `Location` header
- resolved absolute redirect URL
- response time
- content type
- hop-level error

This helps identify whether a tracking service, shortener, CDN, or final landing page caused a failure.

### HEAD and GET behavior

The default `HEAD_THEN_GET` mode starts with HEAD because many URL checks do not need page bodies. If a site rejects HEAD with 403 or 405, the actor retries that hop with GET.

Use `GET_ONLY` if a destination behaves differently for HEAD and GET. Use `HEAD_ONLY` for very low-bandwidth checks when you do not want body requests.

### HTML signals

Enable **Fetch HTML signals** to extract final-page canonical URL and meta robots values.

Keep it disabled for cheapest redirect-only QA. Enable it when SEO or analytics teams need landing-page context.

### Tips for accurate campaign QA

- Use exact expected UTM values from your media plan.
- Set `expectedFinalHost` to catch links landing on staging domains.
- Set `expectedFinalPath` for high-value conversion pages.
- Keep `requireHttps` enabled unless you intentionally test HTTP.
- Increase timeout for slow affiliate or tracking links.
- Lower concurrency for fragile internal QA environments.

### Scheduling monitors

Run the actor on a schedule to detect link drift after launch. For example:

- daily during a campaign launch week
- weekly for evergreen email journeys
- before paid media bulk upload
- after landing-page redirects are changed

### Integrations

#### Google Sheets

Export dataset rows to a Google Sheet and filter `ok = false` for QA follow-up.

#### Slack or email alerts

Use Apify webhooks to notify your team when a scheduled run finds failed rows.

#### Make and Zapier

Trigger the actor from a form, spreadsheet, or campaign-management workflow.

#### Data warehouses

Export JSON or CSV to your warehouse to keep historical QA evidence for campaigns.

### API usage: Node.js

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/utm-campaign-url-qa-auditor').call({
  startUrls: [{ url: 'https://example.com/?utm_source=newsletter&utm_medium=email' }],
  expectedUtmParams: { utm_source: 'newsletter', utm_medium: 'email' },
  expectedFinalHost: 'example.com'
});
console.log(run.defaultDatasetId);
```

### API usage: Python

```python
from apify_client import ApifyClient

client = ApifyClient('YOUR_APIFY_TOKEN')
run = client.actor('automation-lab/utm-campaign-url-qa-auditor').call(run_input={
    'startUrls': [{'url': 'https://example.com/?utm_source=newsletter&utm_medium=email'}],
    'expectedUtmParams': {'utm_source': 'newsletter', 'utm_medium': 'email'},
    'expectedFinalHost': 'example.com'
})
print(run['defaultDatasetId'])
```

### API usage: cURL

```bash
curl -X POST 'https://api.apify.com/v2/acts/automation-lab~utm-campaign-url-qa-auditor/runs?token=YOUR_APIFY_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"startUrls":[{"url":"https://example.com/?utm_source=newsletter&utm_medium=email"}],"expectedUtmParams":{"utm_source":"newsletter","utm_medium":"email"},"expectedFinalHost":"example.com"}'
```

### MCP integration

Use the Apify MCP server from Claude Desktop, Claude Code, or other MCP clients:

```text
https://mcp.apify.com/?tools=automation-lab/utm-campaign-url-qa-auditor
```

Add it in Claude Code:

```bash
claude mcp add apify-utm-url-auditor https://mcp.apify.com/?tools=automation-lab/utm-campaign-url-qa-auditor
```

Claude Desktop JSON config:

```json
{
  "mcpServers": {
    "apify-utm-url-auditor": {
      "url": "https://mcp.apify.com/?tools=automation-lab/utm-campaign-url-qa-auditor"
    }
  }
}
```

Example prompts:

- "Audit these 200 campaign URLs and summarize failed UTM checks."
- "Find every redirect chain with more than two hops."
- "Compare final hosts against my expected domain list."

### Common troubleshooting

#### Why did a row fail with a HEAD error?

Some servers reject HEAD. Keep the default `HEAD_THEN_GET` mode so the actor retries with GET, or switch to `GET_ONLY` for those domains.

#### Why is `ok` false when the final status is 200?

The HTTP request succeeded, but a QA rule failed. Check `missingParams`, `changedParams`, `httpsOk`, `finalHostMatchesExpected`, and `finalPathMatchesExpected`.

#### Why are there extra UTM parameters?

Some tracking systems append their own campaign tags. Add harmless keys to `allowedExtraParams` if you expect them.

### Legality and responsible use

The actor checks public URLs supplied by you. It does not bypass logins, scrape private accounts, or require cookies. Use it only for URLs you are authorized to test and avoid excessive concurrency against fragile sites.

### Related Apify actors

- [Bulk URL Status Checker](https://apify.com/automation-lab/bulk-url-status-checker)
- [Broken Link Checker](https://apify.com/automation-lab/broken-link-checker)
- [Redirect Chain Analyzer](https://apify.com/automation-lab/redirect-chain-analyzer)
- [SEO Audit Tool](https://apify.com/automation-lab/seo-audit-tool)
- [Structured Data Extractor](https://apify.com/automation-lab/structured-data-extractor)

### FAQ

#### Can it audit URL shorteners?

Yes. It manually follows redirects and records each hop.

#### Can it detect stripped UTMs?

Yes. Add expected UTM parameters and inspect `missingParams` and `changedParams`.

#### Can it process hundreds of URLs?

Yes. Set `maxItems` and `concurrency` to match your batch size and target stability.

#### Does it use a browser?

No. It is HTTP-only for speed and cost control.

#### Does it need proxies?

No proxy is configured by default. If a destination blocks automated checks, the actor records a structured error for that URL.

### Changelog

#### 0.1

Initial campaign URL QA actor with redirect-chain capture, UTM validation, host/path checks, HTTPS checks, timings, CSV input, and optional HTML signals.

# Actor input Schema

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

URLs to audit. Each item can be a URL or an object with url, expectedFinalHost, expectedFinalPath, expectedUtmParams, and allowedExtraParams.

## `csvText` (type: `string`):

Optional CSV with columns url, expectedFinalHost, expectedFinalPath, and UTM columns such as utm\_source. Simple comma-separated values only.

## `expectedUtmParams` (type: `object`):

Key-value UTM parameters that should survive redirects, for example {"utm\_source":"newsletter","utm\_medium":"email"}.

## `expectedFinalHost` (type: `string`):

Optional final domain/host to enforce, for example example.com.

## `expectedFinalPath` (type: `string`):

Optional exact final URL path to enforce, for example /pricing.

## `allowedExtraParams` (type: `array`):

UTM parameter names that are allowed even if not listed in expectedUtmParams.

## `requireHttps` (type: `boolean`):

Fail rows whose final landing URL is not HTTPS.

## `includeHtmlSignals` (type: `boolean`):

Fetch the final page with GET to extract canonical URL and meta robots. Leave off for cheapest redirect-only QA.

## `requestMethod` (type: `string`):

HEAD\_THEN\_GET is fastest and falls back to GET when HEAD is blocked.

## `maxRedirects` (type: `integer`):

Stop following a URL after this many redirects.

## `timeoutSecs` (type: `integer`):

Per-hop HTTP timeout.

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

Number of campaign URLs audited in parallel.

## `userAgent` (type: `string`):

Optional user-agent string for sites that reject default clients.

## `maxItems` (type: `integer`):

Safety cap for combined startUrls and csvText rows.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://httpbin.org/redirect-to?url=https%3A%2F%2Fexample.com%2F%3Futm_source%3Dnewsletter%26utm_medium%3Demail"
    },
    {
      "url": "https://example.com/?utm_source=newsletter&utm_medium=email"
    }
  ],
  "expectedUtmParams": {
    "utm_source": "newsletter",
    "utm_medium": "email"
  },
  "expectedFinalHost": "example.com",
  "allowedExtraParams": [],
  "requireHttps": true,
  "includeHtmlSignals": false,
  "requestMethod": "HEAD_THEN_GET",
  "maxRedirects": 10,
  "timeoutSecs": 20,
  "concurrency": 5,
  "maxItems": 20
}
```

# Actor output Schema

## `overview` (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 = {
    "startUrls": [
        {
            "url": "https://httpbin.org/redirect-to?url=https%3A%2F%2Fexample.com%2F%3Futm_source%3Dnewsletter%26utm_medium%3Demail"
        },
        {
            "url": "https://example.com/?utm_source=newsletter&utm_medium=email"
        }
    ],
    "expectedUtmParams": {
        "utm_source": "newsletter",
        "utm_medium": "email"
    },
    "expectedFinalHost": "example.com",
    "allowedExtraParams": [],
    "requireHttps": true,
    "includeHtmlSignals": false,
    "requestMethod": "HEAD_THEN_GET",
    "maxRedirects": 10,
    "timeoutSecs": 20,
    "concurrency": 5,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/utm-campaign-url-qa-auditor").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://httpbin.org/redirect-to?url=https%3A%2F%2Fexample.com%2F%3Futm_source%3Dnewsletter%26utm_medium%3Demail" },
        { "url": "https://example.com/?utm_source=newsletter&utm_medium=email" },
    ],
    "expectedUtmParams": {
        "utm_source": "newsletter",
        "utm_medium": "email",
    },
    "expectedFinalHost": "example.com",
    "allowedExtraParams": [],
    "requireHttps": True,
    "includeHtmlSignals": False,
    "requestMethod": "HEAD_THEN_GET",
    "maxRedirects": 10,
    "timeoutSecs": 20,
    "concurrency": 5,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/utm-campaign-url-qa-auditor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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://httpbin.org/redirect-to?url=https%3A%2F%2Fexample.com%2F%3Futm_source%3Dnewsletter%26utm_medium%3Demail"
    },
    {
      "url": "https://example.com/?utm_source=newsletter&utm_medium=email"
    }
  ],
  "expectedUtmParams": {
    "utm_source": "newsletter",
    "utm_medium": "email"
  },
  "expectedFinalHost": "example.com",
  "allowedExtraParams": [],
  "requireHttps": true,
  "includeHtmlSignals": false,
  "requestMethod": "HEAD_THEN_GET",
  "maxRedirects": 10,
  "timeoutSecs": 20,
  "concurrency": 5,
  "maxItems": 20
}' |
apify call automation-lab/utm-campaign-url-qa-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=automation-lab/utm-campaign-url-qa-auditor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "UTM Campaign URL QA Auditor",
        "description": "Audit campaign URLs in bulk for redirect chains, UTM preservation, HTTPS, final host/path rules, status codes, and timings.",
        "version": "0.1",
        "x-build-id": "zUqQCyHMThUZGQBzg"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/automation-lab~utm-campaign-url-qa-auditor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-automation-lab-utm-campaign-url-qa-auditor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/automation-lab~utm-campaign-url-qa-auditor/runs": {
            "post": {
                "operationId": "runs-sync-automation-lab-utm-campaign-url-qa-auditor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/automation-lab~utm-campaign-url-qa-auditor/run-sync": {
            "post": {
                "operationId": "run-sync-automation-lab-utm-campaign-url-qa-auditor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "startUrls"
                ],
                "properties": {
                    "startUrls": {
                        "title": "Campaign URLs",
                        "type": "array",
                        "description": "URLs to audit. Each item can be a URL or an object with url, expectedFinalHost, expectedFinalPath, expectedUtmParams, and allowedExtraParams.",
                        "items": {
                            "type": "object",
                            "required": [
                                "url"
                            ],
                            "properties": {
                                "url": {
                                    "type": "string",
                                    "title": "URL of a web page",
                                    "format": "uri"
                                }
                            }
                        }
                    },
                    "csvText": {
                        "title": "CSV input (optional)",
                        "type": "string",
                        "description": "Optional CSV with columns url, expectedFinalHost, expectedFinalPath, and UTM columns such as utm_source. Simple comma-separated values only."
                    },
                    "expectedUtmParams": {
                        "title": "Expected UTM parameters",
                        "type": "object",
                        "description": "Key-value UTM parameters that should survive redirects, for example {\"utm_source\":\"newsletter\",\"utm_medium\":\"email\"}."
                    },
                    "expectedFinalHost": {
                        "title": "Expected final host",
                        "type": "string",
                        "description": "Optional final domain/host to enforce, for example example.com."
                    },
                    "expectedFinalPath": {
                        "title": "Expected final path",
                        "type": "string",
                        "description": "Optional exact final URL path to enforce, for example /pricing."
                    },
                    "allowedExtraParams": {
                        "title": "Allowed extra UTM parameters",
                        "type": "array",
                        "description": "UTM parameter names that are allowed even if not listed in expectedUtmParams.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "requireHttps": {
                        "title": "Require HTTPS final URL",
                        "type": "boolean",
                        "description": "Fail rows whose final landing URL is not HTTPS.",
                        "default": true
                    },
                    "includeHtmlSignals": {
                        "title": "Fetch HTML signals",
                        "type": "boolean",
                        "description": "Fetch the final page with GET to extract canonical URL and meta robots. Leave off for cheapest redirect-only QA.",
                        "default": false
                    },
                    "requestMethod": {
                        "title": "Redirect request mode",
                        "enum": [
                            "HEAD_THEN_GET",
                            "GET_ONLY",
                            "HEAD_ONLY"
                        ],
                        "type": "string",
                        "description": "HEAD_THEN_GET is fastest and falls back to GET when HEAD is blocked.",
                        "default": "HEAD_THEN_GET"
                    },
                    "maxRedirects": {
                        "title": "Maximum redirects per URL",
                        "minimum": 0,
                        "maximum": 25,
                        "type": "integer",
                        "description": "Stop following a URL after this many redirects.",
                        "default": 10
                    },
                    "timeoutSecs": {
                        "title": "Timeout per request in seconds",
                        "minimum": 3,
                        "maximum": 120,
                        "type": "integer",
                        "description": "Per-hop HTTP timeout.",
                        "default": 20
                    },
                    "concurrency": {
                        "title": "Concurrent URL checks",
                        "minimum": 1,
                        "maximum": 25,
                        "type": "integer",
                        "description": "Number of campaign URLs audited in parallel.",
                        "default": 5
                    },
                    "userAgent": {
                        "title": "Custom user agent",
                        "type": "string",
                        "description": "Optional user-agent string for sites that reject default clients."
                    },
                    "maxItems": {
                        "title": "Maximum URLs to audit",
                        "minimum": 1,
                        "maximum": 10000,
                        "type": "integer",
                        "description": "Safety cap for combined startUrls and csvText rows.",
                        "default": 20
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
