API

Everything the site does, as JSON. No key, no account; please be gentle.

GET /api/scan?url=<url>

Runs a live scan and returns the full report: score, letter grade, and all 15 checks with plain-English details and fixes.

curl -s 'https://canopystack.dev/api/scan?url=https://example.com' | jq .grade

GET /api/<any scan>?…&fail_under=<grade|score>

The CI gate — now on every scan endpoint. Add fail_under to /api/scan, /api/seo, /api/perf, /api/a11y, /api/links, /api/email, /api/dns, /api/site, or /api/pages and the response answers HTTP 422 (and gate: "fail") when the target scores below the bar — a letter grade (A–F) or a score (0–100). With curl -f, that fails the pipeline step; gate the whole site with one call to /api/site.

# .github/workflows/headers.yml
name: Security headers
on:
  schedule: [{ cron: "0 6 * * 1" }]
  workflow_dispatch: {}
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - name: Fail if grade drops below B
        run: curl -sf 'https://canopystack.dev/api/scan?url=https://example.com&fail_under=B'

GET /api/<any scan>?…&format=md

Every scan endpoint can answer in GitHub-flavored Markdown instead of JSON: the grade, a check-by-check table with ✅/⚠️/❌ icons, and a Fixes section — made to be piped into a CI job summary or posted as a PR comment. Combines with fail_under, so the step that blocks a deploy also shows the report card explaining why.

# In a GitHub Actions step — publish the report card, then gate on the result
# (curl -f alone would discard the report body on a 422):
STATUS=$(curl -s -w '%{http_code}' -o report.md 'https://canopystack.dev/api/scan?url=https://example.com&fail_under=B&format=md')
cat report.md >> "$GITHUB_STEP_SUMMARY"
test "$STATUS" = 200

GET /api/email?domain=<domain>

The email analyzer as JSON: SPF, DMARC, DKIM (8 common selectors), MX, MTA-STS, TLS-RPT, and BIMI, graded like a web scan. Badge at /badge?domain=…, snapshot via /share?domain=….

curl -s 'https://canopystack.dev/api/email?domain=example.com' | jq .grade

GET /api/dns?domain=<domain>

The DNS-posture analyzer as JSON: DNSSEC validation (resolver AD flag), CAA records, nameserver redundancy, IPv6. Badge via /badge?domain=…&type=dns, snapshot via /share?domain=…&type=dns.

curl -s 'https://canopystack.dev/api/dns?domain=example.com' | jq .grade

GET /api/seo?url=<url>

The SEO/meta analyzer as JSON: title, description, canonical, h1, lang, viewport, noindex, robots.txt, sitemap, Open Graph, favicon. Badge via /badge?url=…&type=seo, snapshot via /share?url=…&type=seo.

curl -s 'https://canopystack.dev/api/seo?url=https://example.com' | jq .grade

GET /api/perf?url=<url>

The performance analyzer as JSON: TTFB, document weight, render-blocking scripts, script/style counts, image dimensions and lazy-loading, formats, third-party origins. Badge via /badge?url=…&type=perf, snapshot via /share?url=…&type=perf.

curl -s 'https://canopystack.dev/api/perf?url=https://example.com' | jq '{grade, ttfbMs}'

GET /api/a11y?url=<url>

The accessibility analyzer as JSON: image alt text, form labels, link/button names, pinch-zoom, heading structure, landmarks, skip link, tabindex, link-text quality, frame titles, autoplaying-audio, ARIA-reference resolution, and unique element IDs — 15 WCAG smoke checks. Badge via /badge?url=…&type=a11y, snapshot via /share?url=…&type=a11y.

curl -s 'https://canopystack.dev/api/a11y?url=https://example.com' | jq .grade

GET /api/csp?url=<url>

The Content-Security-Policy deep-dive as JSON: policy presence and enforcement (report-only detected), script-src lockdown, unsafe-inline/unsafe-eval, scheme-wide sources, object-src, base-uri, frame-ancestors — plus the raw policy that was graded. Badge via /badge?url=…&type=csp, snapshot via /share?url=…&type=csp.

curl -s 'https://canopystack.dev/api/csp?url=https://example.com' | jq '{grade, policy}'

GET /api/ai?url=<url>

The AI-crawler readiness analyzer as JSON: llms.txt presence and format, an explicit robots.txt stance on AI crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended, …), blanket-block detection, and whether the page's content is readable without JavaScript. Badge via /badge?url=…&type=ai, snapshot via /share?url=…&type=ai.

curl -s 'https://canopystack.dev/api/ai?url=https://example.com' | jq '{grade, llmsTxt, aiCrawlersNamed}'

GET /api/cookies?url=<url>

The cookie-security analyzer as JSON: every Set-Cookie on an anonymous visit graded per attribute — Secure, HttpOnly on session-looking cookies, SameSite, __Host-/__Secure- prefixes, lifetimes and size budgets — plus the parsed cookie list. Badge via /badge?url=…&type=cookies, snapshot via /share?url=…&type=cookies.

curl -s 'https://canopystack.dev/api/cookies?url=https://example.com' | jq '{grade, cookieCount, cookies}'

GET /api/pwa?url=<url>

The PWA / installability analyzer as JSON: whether a Web App Manifest is linked and valid, name/short_name, 192+512 install icons (and maskable), display mode, start_url, theme color, apple-touch-icon, and viewport — plus the resolved manifest URL and an installable flag. Badge via /badge?url=…&type=pwa, snapshot via /share?url=…&type=pwa.

curl -s 'https://canopystack.dev/api/pwa?url=https://example.com' | jq '{grade, installable, manifestUrl}'

GET /api/privacy?url=<url>

The privacy & tracker analyzer as JSON: named third-party tracking services detected in the page — analytics, cross-site advertising pixels, session recorders — plus any privacy-friendly analytics, detected consent-management platforms, Google-Fonts IP leakage, and the Referrer-Policy. Returns the service names by category. Badge via /badge?url=…&type=privacy, snapshot via /share?url=…&type=privacy.

curl -s 'https://canopystack.dev/api/privacy?url=https://example.com' | jq '{grade, trackers, advertising, consentPlatforms}'

GET /api/site?domain=<domain>

Everything at once: ten analyzers run concurrently and roll into one weighted grade (headers 25%; CSP 10%; cookies 5%; AI readiness 5%; PWA installability 5%; email, DNS, SEO, performance, a11y 10% each). Link health is excluded from the composite — its live probes are too request-hungry — scan it individually, with per-layer grades and issue counts. Badge via /badge?domain=…&type=site, snapshot via /share?domain=…&type=site.

curl -s 'https://canopystack.dev/api/site?domain=example.com' | jq '{grade, layers: (.layers | map_values(.grade))}'

GET /api/links?url=<url>

The link-health analyzer as JSON: up to 20 of the page's links probed live (same-origin first) for dead targets, permanent redirects worth updating, and insecure http:// hrefs. Badge via /badge?url=…&type=links, snapshot via /share?url=…&type=links.

curl -s 'https://canopystack.dev/api/links?url=https://example.com' | jq '{grade, probed}'

GET /api/redirects?url=<url>

The redirect-chain tracer as JSON: every hop with status code, latency, and Location target, the final landing URL, plus findings — loops, chains that dead-end in errors, plain-HTTP downgrades mid-chain, and temporary (302/307) redirects that should be permanent.

curl -s 'https://canopystack.dev/api/redirects?url=http://github.com' | jq '{redirects, finalUrl}'

GET /api/security-txt?url=<domain>

The security.txt (RFC 9116) checker as JSON: whether the file is published at the well-known path, over HTTPS, as text/plain, with a valid Contact and — the field most policies forget — a present, single, unexpired Expires. Returns a state (valid | expired | invalid | malformed | missing), every graded field, the parsed fields, and, when the file is absent or broken, a ready-to-publish suggestion.

curl -s 'https://canopystack.dev/api/security-txt?url=cloudflare.com' | jq '{state, checks: [.checks[] | {label, status}]}'

GET /api/exposure?url=<site>

The exposed-files scanner as JSON: probes ~17 well-known sensitive paths (a public .git or .env, a config backup, a database dump, a site archive, phpinfo(), an open server-status, a .DS_Store) and reports which are actually reachable. Every hit is content-verified — a 200 alone never flags — so a catch-all 404 can't produce a false positive. A single critical exposure caps the grade at F. Badge via /badge?url=…&type=exposure, snapshot via /share?url=…&type=exposure; takes fail_under and format=md.

curl -s 'https://canopystack.dev/api/exposure?url=https://example.com' | jq '{grade, exposedCount, exposed: [.checks[] | select(.status!="pass") | .id]}'

GET /api/robots?url=<site>

The robots.txt validator as JSON: fetches /robots.txt and reads it as a document. Content-verified — a /robots.txt that returns an HTML page (so every rule is silently ignored) is caught, not trusted. Grades an accidental site-wide Disallow: / for a major crawler (caps the grade at F — it deindexes you), misspelled directives, the dead Noindex directive, rules orphaned before any User-agent, an absolute-URL Sitemap declaration, Google's 500 KB parse cap, and Crawl-delay. Badge via /badge?url=…&type=robots, snapshot via /share?url=…&type=robots; takes fail_under and format=md.

curl -s 'https://canopystack.dev/api/robots?url=https://github.com' | jq '{grade, checks: [.checks[] | {id, status}]}'

GET /api/sitemap?url=<site>

The sitemap.xml auditor as JSON: resolves the sitemap the way a crawler does (robots.txt's Sitemap: line, else /sitemap.xml), then reads it as a document. Content-verified — a sitemap that returns an HTML page (so crawlers parse zero URLs) is caught, not trusted. Grades valid sitemap XML, an empty file, Google's 50,000-URL and 50 MB caps, relative or http:// s, cross-host entries, and W3C-datetime validity. Handles a and gzipped sitemaps. Badge via /badge?url=…&type=sitemap, snapshot via /share?url=…&type=sitemap; takes fail_under and format=md.

curl -s 'https://canopystack.dev/api/sitemap?url=https://www.cloudflare.com' | jq '{grade, sitemapUrl, urlCount, checks: [.checks[] | {id, status}]}'

GET /api/schema?type=<type>&…

The structured-data generator as JSON — no fetch, pure compute. Pass a type (Organization, LocalBusiness, Product, FAQPage, Article, BreadcrumbList, Event, WebSite) and its fields as query params; returns the assembled schema.org JSON-LD, a ready flag, and any missing required properties, graded against the same rules the SEO layer applies to live sites. Build valid markup in a script instead of by hand.

curl -s 'https://canopystack.dev/api/schema?type=Product&name=Kettle&price=49.99&ratingValue=4.6&reviewCount=128' | jq '{ready, missing, jsonld}'

POST /api/schema

The other direction: check JSON-LD you already have. POST it as the request body (raw JSON, or a whole