DEV Community

Cizze R
Cizze R

Posted on

3 API Checks I Run Before a Website Launch

3 API Checks I Run Before a Website Launch

A website can return 200 OK and still launch with the wrong certificate, a staging canonical URL, or a mobile layout nobody checked. I use three small API checks to create a timestamped launch evidence pack before DNS cutover or a major release.

The checks cover infrastructure with Domain Intelligence Suite, document metadata with Link Preview & Metadata Extractor, and rendered output with Website Screenshot API. Each tool answers a different question, which keeps the Python orchestration simple and the failures specific.

Use one boring Actor client

All three Actors can return a dataset item through Apify's synchronous endpoint. A shared client handles authentication, timeouts, and response validation without hiding which Actor failed.

import os
from typing import Any

import requests

APIFY_TOKEN = os.getenv("APIFY_TOKEN", "YOUR_APIFY_TOKEN")
API_ROOT = "https://api.apify.com/v2/acts"


def run_actor(actor_id: str, payload: dict[str, Any]) -> dict[str, Any]:
    url = f"{API_ROOT}/{actor_id}/run-sync-get-dataset-items"
    response = requests.post(
        url,
        params={"token": APIFY_TOKEN},
        json=payload,
        timeout=120,
    )
    response.raise_for_status()

    items = response.json()
    if not isinstance(items, list) or len(items) != 1:
        raise RuntimeError(
            f"{actor_id} returned {len(items) if isinstance(items, list) else 'invalid'} items"
        )
    return items[0]
Enter fullscreen mode Exit fullscreen mode

In CI, set APIFY_TOKEN through the platform's secret store. The fallback placeholder makes the example copyable without encouraging tokens in source control.

Check 1: DNS and TLS match the launch plan

Before checking page content, confirm that the domain resolves and presents a healthy certificate. The domain Actor runs DNS and SSL modules independently, so the code checks each module for its own error.

DOMAIN_ACTOR = "weeknds~domain-intelligence-suite"


def check_domain(domain: str, minimum_tls_days: int = 14) -> dict:
    result = run_actor(
        DOMAIN_ACTOR,
        {
            "domain": domain,
            "modules": ["dns", "ssl"],
            "dnsRecordTypes": ["A", "AAAA", "CNAME", "NS"],
            "sslPort": 443,
        },
    )

    findings = []
    dns = result.get("dns", {})
    ssl = result.get("ssl", {})

    if dns.get("error"):
        findings.append(f"DNS lookup failed: {dns['error']}")
    else:
        records = dns.get("records", {})
        if not any(records.get(kind) for kind in ("A", "AAAA", "CNAME")):
            findings.append("No A, AAAA, or CNAME record found")

    if ssl.get("error"):
        findings.append(f"TLS check failed: {ssl['error']}")
    else:
        certificate = ssl.get("certificate", {})
        if certificate.get("expired"):
            findings.append("TLS certificate is expired")
        days = certificate.get("days_remaining")
        if isinstance(days, int) and days < minimum_tls_days:
            findings.append(f"TLS certificate expires in {days} days")

    return {
        "passed": not findings,
        "findings": findings,
        "evidence": result,
    }
Enter fullscreen mode Exit fullscreen mode

This is a launch gate, not a full security audit. It verifies the routing and certificate facts most likely to be broken during cutover. Keep expected IP addresses or nameservers in release configuration if the job must also prove that DNS points to a particular provider.

Check 2: Validate what crawlers will read

A rendered page can look correct while its canonical, description, or social image still references staging. The metadata Actor fetches the deployed URL over HTTP and returns document metadata plus response details.

The response can expose fields as nested data or literal Open Graph keys, so a small recursive search keeps the policy independent from presentation.

from collections.abc import Mapping
from urllib.parse import urlparse

METADATA_ACTOR = "weeknds~link-preview-metadata-extractor"


def find_value(value: Mapping, *wanted: str):
    for key, child in value.items():
        if str(key) in wanted and child not in (None, "", []):
            return child
        if isinstance(child, Mapping):
            match = find_value(child, *wanted)
            if match not in (None, "", []):
                return match
    return None


def check_metadata(url: str, expected_host: str) -> dict:
    result = run_actor(
        METADATA_ACTOR,
        {
            "url": url,
            "includeFavicon": True,
            "userAgent": "Launch-Evidence-Check/1.0",
            "timeout": 20,
        },
    )
    findings = []

    if result.get("error"):
        findings.append(result["error"])
    else:
        status = find_value(result, "status_code")
        title = find_value(result, "og:title", "title")
        canonical = find_value(result, "canonical_url", "canonical", "og:url")
        social_image = find_value(result, "og:image")
        robots = str(find_value(result, "robots") or "").lower()

        if status and not 200 <= int(status) < 300:
            findings.append(f"Metadata request returned HTTP {status}")
        if not title:
            findings.append("No document or Open Graph title found")
        if not canonical:
            findings.append("Canonical URL is missing")
        elif urlparse(str(canonical)).hostname != expected_host:
            findings.append(f"Canonical URL does not use {expected_host}")
        if not social_image or urlparse(str(social_image)).scheme != "https":
            findings.append("Open Graph image must be an absolute HTTPS URL")
        if "noindex" in robots:
            findings.append("Robots metadata contains noindex")

    return {
        "passed": not findings,
        "findings": findings,
        "evidence": result,
    }
Enter fullscreen mode Exit fullscreen mode

The title and social image requirements are product policy, not web standards. Adjust the rules for a private dashboard, documentation site, or marketing page, but keep the expected canonical host explicit.

Check 3: Capture the layouts humans will see

Metadata checks cannot catch a cookie banner covering the call to action or a navigation menu overflowing on mobile. I capture desktop light mode and mobile dark mode as a compact visual pair.

from pathlib import Path

SCREENSHOT_ACTOR = "weeknds~website-screenshot-api"


def capture_view(url: str, viewport: str, dark_mode: bool) -> dict:
    result = run_actor(
        SCREENSHOT_ACTOR,
        {
            "url": url,
            "viewport": viewport,
            "fullPage": True,
            "delay": 1500,
            "format": "png",
            "darkMode": dark_mode,
        },
    )
    if not result.get("success") or not result.get("screenshotUrl"):
        raise RuntimeError(result.get("error") or "Screenshot capture failed")
    return result


def download_screenshot(result: dict, destination: Path) -> Path:
    response = requests.get(result["screenshotUrl"], timeout=60)
    response.raise_for_status()
    if not response.headers.get("content-type", "").startswith("image/"):
        raise RuntimeError("Screenshot URL did not return an image")

    destination.parent.mkdir(parents=True, exist_ok=True)
    temporary = destination.with_suffix(destination.suffix + ".tmp")
    temporary.write_bytes(response.content)
    temporary.replace(destination)
    return destination
Enter fullscreen mode Exit fullscreen mode

A fixed viewport and delay make repeated captures easier to compare. Dynamic ads, clocks, personalization, and animations can still change between runs, so visual evidence needs human review rather than a byte-for-byte equality gate.

Assemble a timestamped evidence pack

The orchestrator runs all checks, saves the complete structured responses, and downloads screenshots next to the JSON. It exits non-zero when an automated gate fails while still preserving evidence from completed checks.

import json
from datetime import datetime, timezone
from pathlib import Path


def build_launch_pack(url: str, domain: str, output_root: Path) -> Path:
    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    pack = output_root / f"{timestamp}-{domain}"
    pack.mkdir(parents=True, exist_ok=False)

    checks = {
        "domain": check_domain(domain),
        "metadata": check_metadata(url, domain),
    }

    screenshot_specs = [
        ("desktop-light", "desktop", False),
        ("mobile-dark", "mobile", True),
    ]
    screenshots = {}
    for name, viewport, dark_mode in screenshot_specs:
        result = capture_view(url, viewport, dark_mode)
        path = download_screenshot(result, pack / f"{name}.png")
        screenshots[name] = {"file": path.name, "actor_result": result}

    report = {
        "url": url,
        "domain": domain,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "checks": checks,
        "screenshots": screenshots,
    }
    (pack / "report.json").write_text(
        json.dumps(report, indent=2, sort_keys=True),
        encoding="utf-8",
    )

    failed = [name for name, result in checks.items() if not result["passed"]]
    if failed:
        raise RuntimeError("Launch checks failed: " + ", ".join(failed))
    return pack


if __name__ == "__main__":
    evidence = build_launch_pack(
        "https://www.example.com/",
        "www.example.com",
        Path("launch-evidence"),
    )
    print(evidence)
Enter fullscreen mode Exit fullscreen mode

Run this against the exact public hostname users will visit. If DNS is still private before cutover, run it from an authorized network or use a temporary validation hostname and repeat the check after the public change.

Pricing and release timing

Current Store pricing starts at $5 per 1,000 domain intelligence results for Domain Intelligence Suite, $2 per 1,000 extractions for Link Preview & Metadata Extractor, and $3 per 1,000 captures for Website Screenshot API. One pass with one domain check, one metadata extraction, and two screenshots is $0.013 in Actor charges, so 100 passes are $1.30. Apify platform usage may also apply; confirm live pricing before a high-volume rollout.

Do not run the pack only before deployment. Run it once after the public DNS or CDN change has propagated, because that is when resolver paths, production headers, certificates, and canonical URLs can differ from staging.

Store the evidence pack with the release ID and a short retention period. Review both images at their native size, verify any flagged field against the raw Actor response, and record the approver separately so a screenshot folder is never mistaken for an approval decision.

Top comments (0)