How to Audit All of a Campaign's Links at Once for Broken Destinations (CLI / CI)

A three-stage pipeline: a links.txt file card with five campaign URLs feeds into a bash loop running mlz check on each URL, then into a results panel showing four PASS rows and one FAIL row with exit code 1

To audit campaign links across a whole campaign at once, loop mlz check from MissingLinkz — campaign link infrastructure — over your list of URLs in a shell script or CI job. Each invocation validates one URL for SSL, redirect chain integrity, and HTTP resolution; each exits non-zero on failure. A simple while loop accumulates the failures and exits with a non-zero code if any link fails, giving you a single pass/fail verdict across the entire batch.

Note: mlz validates one URL per invocation — there is no --batch or --file flag. The "bulk" audit is the loop or CI job you build around it, not a hidden mode inside the tool. This is by design: each URL goes through an independent HTTP request chain, and the script wrapper gives you full control over logging, retry logic, and exit behavior.

That said, the automated loop catches failures that manual checking consistently misses: a link that opened fine in your browser yesterday but now resolves to a 404, a redirect hop that strips UTM parameters and corrupts attribution, an expired SSL certificate, or a slow destination that will hurt Quality Score. Auditing a campaign of 20 links by hand — pasting each into a browser, clicking through, eyeballing the page — takes 20 minutes, is unrepeatable, and misses the failures a browser silently works around. The loop runs in seconds and fails the build if any destination is broken.

Why manual campaign link auditing doesn't scale

A typical mid-size campaign has 10–50 landing page URLs across ad groups, email variants, social posts, and retargeting audiences. Before launch, each URL needs to be verified: does the destination exist, is SSL configured, do redirect hops preserve the query string, does the page respond fast enough to avoid a Quality Score penalty?

Manual checking fails in three predictable ways:

It's slow and unrepeatable
Opening 30 URLs in browser tabs and clicking through each one takes 20–30 minutes. Run the same audit the next day after a content update and you're starting over. There's no record of what passed or failed, no exit code to fail a deploy on, and no diff to show what changed between runs.
Browsers mask failures
A browser cached the previous response from a URL that now returns 404. A redirect loop that takes 8 hops resolves visually in the tab but would cause a robot crawl to time out. An SSL certificate expired last night but the browser shows a cached HSTS response. mlz check makes a fresh HTTP request every time, with no cache, from outside your network.
Parameter-only validators skip the destination
Web-based UTM "validators" check that your parameter names follow naming rules — they never actually fetch the URL. A parameter-perfectly-formed link that points to a broken landing page passes their check. The link is broken in production; the validator said it was fine. See campaign link validation for a full breakdown of what destination validation catches that parameter validators miss.

What mlz check validates per URL

mlz check is MissingLinkz's destination validation command. It takes a full URL — including any UTM parameters already appended — makes a real HTTP request chain, and reports five checks:

URL format
Is the URL syntactically valid? Catches encoding errors, double-slashes, and malformed parameter strings that would silently fail in GA4.
SSL / HTTPS
Does the URL use HTTPS? HTTP links in paid campaigns trigger browser security warnings and lower Quality Score on most ad platforms.
HTTP resolution
Does the destination return a 200 OK? Catches 404s, 410s, 500s, and any status that means the campaign is driving traffic to a broken page.
Redirect chain
How many hops are there? Are the hops clean? Catches redirect loops, chains that exceed browser limits, and hops that rewrite the URL and drop the query string — the most common cause of UTM attribution loss that a browser quietly follows but GA4 never records correctly.
Response time
Is the page responding within a normal window? Slow destinations increase bounce rate and reduce ad platform Quality Score.

For OG tag validation, Twitter Card checks, and the full pre-publish go/no-go verdict, use mlz preflight — it builds the UTM link and runs all checks in one command. For an audit loop over pre-built campaign URLs, mlz check is the right tool.

Building the audit loop: step by step

Step 1: Create your links file

Put one fully-built campaign URL per line in a plain text file. Include any UTM parameters already appended — mlz check takes the full URL as-is.

links.txt
https://example.com/landing-a?utm_source=google&utm_medium=cpc&utm_campaign=q3-launch
https://example.com/landing-b?utm_source=linkedin&utm_medium=social&utm_campaign=q3-launch
https://example.com/landing-c?utm_source=email&utm_medium=email&utm_campaign=q3-launch
https://bit.ly/spring-promo-q3
https://example.com/q3-offer?utm_source=tiktok&utm_medium=paid-social&utm_campaign=q3-launch

Shortlinks (like the Bit.ly URL above) are valid inputs — mlz check follows every redirect hop and reports the final destination. This is exactly the failure mode most manual audits miss: the shortened URL resolves fine visually but the destination behind it has gone 404 or the redirect strips the UTM query string.

Step 2: Run the audit script

audit-links.sh
$ cat audit-links.sh
#!/bin/bash
FAIL=0
while IFS= read -r url; do
  echo -n "Checking $url ... "
  if mlz check "$url" --format json > /tmp/mlz_result.json 2>&1; then
    echo "PASS"
  else
    echo "FAIL"
    cat /tmp/mlz_result.json
    FAIL=1
  fi
done < links.txt
echo ""
echo "Audit complete. Exit: $FAIL"
exit $FAIL

$ bash audit-links.sh
Checking https://example.com/landing-a?... PASS
Checking https://example.com/landing-b?... PASS
Checking https://example.com/landing-c?... PASS
Checking https://bit.ly/spring-promo-q3 ... FAIL
"resolution": "fail", "message": "Destination returned 404."
Checking https://example.com/q3-offer?... PASS

Audit complete. Exit: 1

The script reads each URL from links.txt, runs mlz check against it, and accumulates any failures. It exits with code 1 if any URL failed — the CI-native signal for "this job failed; block the deploy."

The JSON response: what each check returns

When you pass --format json, mlz check returns a structured object with a valid boolean and an array of per-check results. Here is the response for a passing URL:

mlz check (PASS)
{
  "url": "https://example.com/landing-a?utm_source=google&utm_medium=cpc&utm_campaign=q3",
  "valid": true,
  "checks": [
    { "check": "url_format", "status": "pass", "message": "URL format is valid." },
    { "check": "ssl", "status": "pass", "message": "URL uses HTTPS." },
    { "check": "resolution", "status": "pass", "message": "Destination responded with 200.",
      "details": { "status_code": 200, "response_time_ms": 214 } },
    { "check": "redirects", "status": "pass", "message": "No redirects detected." },
    { "check": "response_time", "status": "pass", "message": "Response time: 214ms." }
  ],
  "status_code": 200,
  "response_time_ms": 214,
  "validated_at": "2026-07-31T09:14:18.000Z"
}

Here is the same command run against a broken shortlink whose destination has gone 404:

mlz check (FAIL)
{
  "url": "https://bit.ly/spring-promo-q3",
  "valid": false,
  "checks": [
    { "check": "url_format", "status": "pass", "message": "URL format is valid." },
    { "check": "ssl", "status": "pass", "message": "URL uses HTTPS." },
    { "check": "resolution", "status": "fail", "message": "Destination returned 404.",
      "details": { "status_code": 404, "response_time_ms": 289 } },
    { "check": "redirects", "status": "warn", "message": "2 redirect hops detected.",
      "details": { "hop_count": 2 } },
    { "check": "response_time", "status": "pass", "message": "Response time: 289ms." }
  ],
  "status_code": 404,
  "response_time_ms": 289,
  "validated_at": "2026-07-31T09:14:22.000Z"
}

The valid: false field and the non-zero exit code from mlz check are what the loop uses to accumulate the overall FAIL state. The JSON output lets you pipe the results into jq, write them to a report file, or send them to Slack as part of a notification step.

Running the audit in CI/CD

The loop pattern translates directly to any CI system. Here is a minimal GitHub Actions job that reads links.txt from the repo, runs the audit, and fails the workflow if any link is broken. See automating campaign link validation in CI/CD for a fuller template with Slack notifications and artifact upload.

.github/workflows/campaign-link-audit.yml
name: Campaign Link Audit
on:
  schedule:
    - cron: '0 7 * * 1-5'   # nightly weekday run
  workflow_dispatch:          # trigger manually before launch

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm install -g missinglinkz
      - name: Audit campaign links
        run: |
          FAIL=0
          while IFS= read -r url; do
            echo -n "Checking $url ... "
            mlz check "$url" || FAIL=1
          done < links.txt
          exit $FAIL
        env:
          MLZ_API_KEY: ${{ secrets.MLZ_API_KEY }}

Two scheduling patterns are common:

Pre-launch gate
Trigger the audit manually (workflow_dispatch) or as a required check on the PR that updates your campaign configuration. The build fails if any destination is broken; the campaign doesn't go live until the audit passes.
Nightly cron
Run the audit every weekday morning against live campaign links. A destination that went 404 overnight — a page deleted after a site migration, an offer that expired and got unpublished — pages the on-call marketer before ad spend starts flowing to a broken page. This is the problem detailed in how broken campaign links waste your ad budget.

Upgrading to mlz preflight for the full go/no-go

mlz check validates the destination. mlz preflight goes further: it builds the UTM link from source, medium, and campaign arguments and then runs the full suite of checks including OG tags, Twitter Card metadata, viewport, SSL, redirect chain, and UTM parameter survival through every redirect hop. For the complete pre-publish story on a single URL, see why every campaign link needs a preflight check.

The tradeoff: mlz preflight requires --source, --medium, and --campaign as separate arguments, so you can't feed it a pre-built URL from a links.txt without parsing out the UTM parameters first. For auditing a list of already-built campaign URLs, mlz check in the loop is the right tool. For the full pre-publish check on a URL you're about to build, use mlz preflight.

Audit your campaign links before any broken destination costs you ad spend

MissingLinkz is campaign link infrastructure: install the CLI, put your URLs in a file, and run the audit loop in seconds — locally or in CI.

npm install -g missinglinkz

Then: while IFS= read -r url; do mlz check "$url" || FAIL=1; done < links.txt && exit $FAIL

Recommended posts

FAQ

How do I check multiple UTM links at once?
Put each fully-built campaign URL on its own line in a text file (e.g. links.txt), then loop mlz check over it: while IFS= read -r url; do mlz check "$url" || FAIL=1; done < links.txt; exit $FAIL. mlz check validates one URL per invocation; the shell loop is what makes it a bulk audit. The script exits non-zero if any URL fails, so it works as a CI gate or a pre-launch script.
Does mlz check have a --batch or --file flag?
No. mlz check validates a single URL per invocation. The bulk audit is the while loop or CI step you wrap around it, not a flag inside the tool. This is by design: each URL is an independent HTTP request chain, and the loop gives you control over logging, retry logic, and per-URL output.
What does mlz check validate?
mlz check validates URL format, SSL/HTTPS configuration, HTTP resolution (destination returns 200 OK), redirect chain integrity (number of hops, clean resolution), and response time. It does not validate OG tags, Twitter Cards, or UTM parameter survival through redirects — for those, use mlz preflight.
How do I use this audit in GitHub Actions?
Add a workflow file with a step that installs MissingLinkz (npm install -g missinglinkz) and then runs the while loop against a links.txt file committed to your repo. Set your API key as a GitHub secret (MLZ_API_KEY) and reference it in the step's env block. The workflow exits non-zero if any link fails, which marks the run as failed. See automating campaign link validation in CI/CD for a complete template.
When should I use mlz preflight instead of mlz check?
Use mlz preflight when you want the full pre-publish go/no-go verdict on a link you're about to build: it takes --url, --source, --medium, and --campaign as separate arguments, builds the UTM-tagged URL, and runs all checks including OG tags, Twitter Cards, viewport, and UTM survival through the redirect chain. Use mlz check in the loop when you already have a list of fully-built campaign URLs and want to audit their destinations at scale.