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.
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: 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 bymlz buildin 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=cpccampaigns. 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
{
"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.
{
"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.
truemeans all required checks passed.falsemeans at least one check failed — inspect thechecksarray for which check has"status": "fail". Warnings do not setreadyto 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
checkname, astatusof"pass","warn", or"fail", a human-readablemessage, and an optionaldetailsobject 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 -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:
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.
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:
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:
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 checkworks offline for URL-only validation), the REST API always requires a validAuthorization: Bearerheader. The free plan gives you 50 calls per month — register withmlz 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 checkormlz publish-checkvia 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 200with a response body, but"ready": false— theresolutioncheck 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/preflightcall 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 on5xxerrors. Use exponential backoff with jitter: start at 1 second, cap at 30 seconds.
Related reading
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.