MissingLinkz REST API: Validate Campaign Links Programmatically

The campaign link validation REST API lets you integrate pre-publish checks directly into server-side pipelines, custom dashboards, and backend services — without installing the CLI. A single POST /v1/preflight request returns a ready boolean alongside a structured JSON report covering SSL, URL resolution, redirect chain, UTM parameter integrity, Open Graph tags, and Twitter Cards. This article is the complete reference: authentication, request shape, response schema, curl and Node.js examples, and a guide to choosing between the REST API, the CLI, and the MCP server.

Split illustration: a terminal window on the left shows a curl POST request to api.missinglinkz.io/v1/preflight, connected by a green dashed arrow to a white JSON response panel on the right showing ready: true and a list of passed checks

REST API vs CLI vs MCP: which integration fits your use case?

MissingLinkz exposes three integration surfaces. Choosing the right one depends on where your validation logic lives:

Use case CLI MCP server REST API
Local terminal, ad-hoc checks Best fit Overkill Overkill
GitHub Actions / GitLab CI Good Not designed for CI Good
Server-side script or backend service Requires Node install Not designed for servers Best fit
AI agent (Claude, Cursor, custom) No structured tool call Best fit Works via API call
Custom dashboard or internal tool Hard to embed Wrong transport Best fit
No Node.js on the target machine Requires Node Requires Node HTTP-only

The rule of thumb: use the CLI for local development and CI, the MCP server for AI agent tool calls, and the REST API when you need to call validation from a language or environment where installing an npm package is not practical.

Authentication

All REST API requests require an API key passed in the Authorization header as a Bearer token. Get your key by registering with the CLI:

mlz auth register --email [email protected]

Or check your existing key:

mlz auth status

Store the key as an environment variable. Every request must include:

Authorization header
Authorization: Bearer mlz_live_your_key_here
Content-Type: application/json

A missing or invalid key returns HTTP 401 with {"error": "unauthorized"}. The free plan allows 50 API calls per month with no credit card required. Quotas reset on the first of each calendar month.

POST /v1/preflight — request format

This single endpoint covers the full pre-publish validation workflow: it builds the UTM-tracked link, validates the destination URL, and inspects the landing page for social sharing readiness. The base URL is https://api.missinglinkz.io.

Required parameters

url (string)
The destination URL to validate. Must include the scheme (https://). The URL is not required to have UTM parameters already — those are passed as separate fields and appended by the API.
utm_source (string)
The traffic source. Examples: linkedin, google, newsletter. The API normalises values to lowercase-hyphenated format before validating — matching the same convention enforced by mlz build in the CLI.
utm_medium (string)
The marketing medium. Examples: social, cpc, email.
utm_campaign (string)
The campaign name. Examples: spring-launch-2026, q2-brand.

Optional parameters

utm_term (string)
Paid search keyword. Only needed for utm_medium=cpc campaigns.
utm_content (string)
Content variant identifier for A/B testing. Example: hero-cta-v2.
utm_id (string)
Cross-platform click ID. Used when integrating with ad platform attribution.

Minimal request example

POST /v1/preflight — minimal
{
  "url":          "https://yoursite.com/landing",
  "utm_source":   "linkedin",
  "utm_medium":   "social",
  "utm_campaign": "spring-2026"
}

Response schema

A successful request returns HTTP 200 with a JSON body. The top-level ready boolean is the single signal your pipeline needs. Everything below it is the detail that explains why.

200 OK — response body
{
  "ready": true,
  "tracked_url": "https://yoursite.com/landing?utm_source=linkedin&utm_medium=social&utm_campaign=spring-2026",
  "destination_url": "https://yoursite.com/landing",
  "params": {
    "utm_source":   "linkedin",
    "utm_medium":   "social",
    "utm_campaign": "spring-2026"
  },
  "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": 241 } },
    { "check": "redirects",     "status": "pass", "message": "No redirects detected. UTM parameters preserved.",
      "details": { "hops": 0, "utm_preserved": true } },
    { "check": "response_time", "status": "pass", "message": "Response time: 241ms." },
    { "check": "open_graph",    "status": "pass", "message": "Open Graph tags present: title, description, and image." },
    { "check": "twitter_card",  "status": "pass", "message": "Twitter Card present (type: summary_large_image)." },
    { "check": "viewport",      "status": "pass", "message": "Viewport meta tag present." }
  ],
  "summary": {
    "total": 12, "passed": 12, "warnings": 0, "failed": 0
  },
  "recommendation": "All checks passed. Campaign link is ready to publish.",
  "link_id":    "lnk_9wlvd9qi",
  "campaign_id": "cmp_kbcht77d",
  "created_at": "2026-04-28T09:00:00.000Z"
}

Key fields to understand:

ready (boolean)
The go/no-go verdict. true means all required checks passed. false means at least one check failed — inspect the checks array for which check has "status": "fail". Warnings do not set ready to false.
tracked_url (string)
The assembled UTM-tagged URL ready to copy and publish. This is the URL with your parameters appended using the normalised values.
checks (array)
Each check object has a check name, a status of "pass", "warn", or "fail", a human-readable message, and an optional details object with numeric data. Use the status field for programmatic gating — don't parse the message string.
summary (object)
Aggregate counts: total, passed, warnings, failed. Useful for logging or dashboarding without iterating every check.

curl example

The simplest way to call the API from the terminal or a shell script. Replace $MLZ_API_KEY with your key or export it as an environment variable.

curl — POST /v1/preflight
$ curl -s -X POST https://api.missinglinkz.io/v1/preflight \
  -H "Authorization: Bearer $MLZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/landing",
    "utm_source": "linkedin",
    "utm_medium": "social",
    "utm_campaign": "spring-2026"
  }' | jq '.ready, .tracked_url, .summary'

To gate a shell script on the result, check the exit code from jq -e:

shell gate pattern
RESULT=$(curl -s -X POST https://api.missinglinkz.io/v1/preflight \
  -H "Authorization: Bearer $MLZ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://yoursite.com/landing","utm_source":"linkedin","utm_medium":"social","utm_campaign":"spring-2026"}')

echo "$RESULT" | jq '.'

if echo "$RESULT" | jq -e '.ready == true' > /dev/null 2>&1; then
  echo "Link is ready to publish."
else
  echo "Validation failed. Check the report above."
  exit 1
fi

Node.js example

For server-side JavaScript, use the built-in fetch API (Node.js 18+) or any HTTP client. No additional dependencies needed beyond what ships with modern Node.

validate-campaign-link.js
async function validateCampaignLink(params) {
  const response = await fetch('https://api.missinglinkz.io/v1/preflight', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MLZ_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(params),
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`API error ${response.status}: ${err.error}`);
  }

  return response.json();
}

const report = await validateCampaignLink({
  url:          'https://yoursite.com/landing',
  utm_source:   'linkedin',
  utm_medium:   'social',
  utm_campaign: 'spring-2026',
});

if (report.ready) {
  console.log('Ready:', report.tracked_url);
} else {
  const failed = report.checks.filter(c => c.status === 'fail');
  console.error('Validation failed:', failed);
  process.exit(1);
}

For bulk validation — checking multiple campaign links in a deployment pipeline — loop over an array and accumulate failures before deciding whether to exit:

bulk validation loop
const campaigns = [
  { url: 'https://yoursite.com/q2',     utm_source: 'linkedin', utm_medium: 'social',  utm_campaign: 'q2-launch' },
  { url: 'https://yoursite.com/brand',  utm_source: 'google',   utm_medium: 'cpc',     utm_campaign: 'brand-2026' },
  { url: 'https://yoursite.com/email',  utm_source: 'mailchimp', utm_medium: 'email',   utm_campaign: 'newsletter-may' },
];

const results = await Promise.all(campaigns.map(validateCampaignLink));
const failures = results.filter(r => !r.ready);

if (failures.length) {
  console.error(`${failures.length} campaign link(s) failed validation`);
  process.exit(1);
}
console.log(`All ${results.length} campaign links validated.`);

Python example

For Python environments — data pipelines, marketing automation scripts, or Django/FastAPI backends — use the standard requests library:

validate_campaign_link.py
import os
import requests
import sys

API_URL = "https://api.missinglinkz.io/v1/preflight"
API_KEY = os.environ["MLZ_API_KEY"]

def validate_campaign_link(url, utm_source, utm_medium, utm_campaign, **kwargs):
    resp = requests.post(
        API_URL,
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "url":          url,
            "utm_source":   utm_source,
            "utm_medium":   utm_medium,
            "utm_campaign": utm_campaign,
            **kwargs,
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()

report = validate_campaign_link(
    url="https://yoursite.com/landing",
    utm_source="linkedin",
    utm_medium="social",
    utm_campaign="spring-2026",
)

if report["ready"]:
    print(f"Ready: {report['tracked_url']}")
else:
    failed = [c for c in report["checks"] if c["status"] == "fail"]
    print(f"Validation failed: {failed}", file=sys.stderr)
    sys.exit(1)

Error codes

Non-2xx responses indicate a problem with the request itself, not the campaign link being validated. All errors return a JSON body with an error string.

HTTP status Error string Cause
400 "bad_request" Malformed JSON body or missing required fields (url, utm_source, utm_medium, utm_campaign)
401 "unauthorized" Missing or invalid Authorization header
422 "invalid_url" The url field is not a valid URL (no scheme, malformed hostname, etc.)
429 "quota_exceeded" Monthly API call quota exhausted for your plan tier
500 "internal_error" Unexpected server error — retry with exponential backoff

A 429 response means you have reached your plan's monthly quota, not a per-second rate limit. The response body includes a reset_at field with the ISO 8601 timestamp when the quota resets. Upgrade your plan or wait for the monthly reset. Plan quotas: Free 50/month, Agent 2,000/month, Pro 20,000/month, Enterprise unlimited.

FAQ

Do I need an API key to call the REST API?
Yes. Unlike the CLI (mlz check works offline for URL-only validation), the REST API always requires a valid Authorization: Bearer header. The free plan gives you 50 calls per month — register with mlz auth register --email [email protected] to get a key immediately, no credit card required.
Can I call the REST API without installing the CLI?
Yes. The REST API is a standalone HTTP endpoint. You only need the CLI if you want to generate API keys (mlz auth register) or run local validation without making network calls. Once you have a key, you can call the API from any language or environment that can make HTTP POST requests.
How does the REST API compare to calling mlz preflight in CI?
For GitHub Actions and similar CI systems, mlz check or mlz publish-check via the CLI is simpler because it handles authentication, JSON output, and exit codes in one npm install step. The REST API is the better choice when your CI pipeline is in a language other than Node.js, when you're calling from a backend service that already manages HTTP clients, or when you want to avoid adding npm dependencies. See the GitHub Actions template for the CLI-based CI pattern.
What happens if the destination URL is down when I call the API?
The API will still return HTTP 200 with a response body, but "ready": false — the resolution check will show "status": "fail" with the HTTP status code returned (or a timeout error). This is the expected behavior: a destination that's down at validation time is not ready to receive campaign traffic, even if the UTM parameters are correctly formatted.
Are requests idempotent? Can I retry on failure?
Yes. Each POST /v1/preflight call is independent — it performs a fresh HTTP check against the destination URL and does not modify any state on your account (other than incrementing your quota counter). Safe to retry on 5xx errors. Use exponential backoff with jitter: start at 1 second, cap at 30 seconds.

Start calling the REST API

Get your API key in 30 seconds with the CLI, then call the endpoint from any environment. Free plan: 50 validations per month, no credit card required.

npm install -g missinglinkz
mlz auth register --email [email protected]

Then call POST https://api.missinglinkz.io/v1/preflight with your key. See the SKILL.md reference for the full command list.