Skip to main content

REST API

Plain HTTP+JSON endpoints, callable from any language with a Bearer API key. No SDK required.

Base URL: https://sitehealth.octagramlabs.com.

POST /api/external/budget-check

Runs a pass/fail check of the latest completed scan against the site's configured performance budgets and alert rules. Designed to drop into a CI pipeline (GitHub Actions, GitLab CI, CircleCI) — exit on the pass field.

Auth

Authorization: Bearer wsh_… (API key only; OAuth tokens are not accepted on this endpoint).

Request schema

FieldTypeRequiredDescription
siteIdstring (uuid)yesSite to check. Get it from the Settings page or MCP list_sites.
POST /api/external/budget-check HTTP/1.1
Host: sitehealth.octagramlabs.com
Authorization: Bearer wsh_a1b2c3d4e5f6…
Content-Type: application/json

{ "siteId": "11111111-2222-3333-4444-555555555555" }

Response schema

FieldTypeDescription
passbooleantrue if no violations. Use this as the CI gate.
sitestringSite name
scoresobject{ health, performance, seo, accessibility, bestPractices } — 0–100
violationsarrayEach: { metric, threshold, actual, operator }
scanIdstring (uuid)The scan the check was run against
scannedAtISO timestampWhen the scan completed

Example response (failing)

{
"pass": false,
"site": "Acme Inc",
"scores": {
"health": 74,
"performance": 68,
"seo": 95,
"accessibility": 91,
"bestPractices": 100
},
"violations": [
{ "metric": "performance", "threshold": 80, "actual": 68, "operator": "min" },
{ "metric": "lcp", "threshold": 2500, "actual": 3420, "operator": "max" }
],
"scanId": "aaaa-bbbb-cccc-dddd-eeee",
"scannedAt": "2026-04-17T10:05:00.000Z"
}

Status codes

CodeWhen
200Check ran (including pass: false). Also returned if the site has no completed scans yet (pass: false, no scores).
400Body was not valid JSON
401Missing or invalid API key
404Site not found or not owned by the API key's user
422siteId is not a valid UUID

CI integration example (GitHub Actions)

- name: Performance budget check
env:
SITE_HEALTH_KEY: ${{ secrets.SITE_HEALTH_API_KEY }}
SITE_ID: 11111111-2222-3333-4444-555555555555
run: |
response=$(curl -sS -X POST \
https://sitehealth.octagramlabs.com/api/external/budget-check \
-H "Authorization: Bearer $SITE_HEALTH_KEY" \
-H "Content-Type: application/json" \
-d "{\"siteId\":\"$SITE_ID\"}")
echo "$response" | jq .
echo "$response" | jq -e '.pass == true'

jq -e exits non-zero if pass is not true, failing the step.

tip

Configure your budgets in Settings → Site → Budgets before relying on this endpoint. With no budgets set, violations will always be empty and pass will always be true.

GET /api/sites/{siteId}/export

Download scan results as CSV or JSON for bulk analysis / reporting.

Auth

Session cookie only (this endpoint is primarily UI-driven from the "Export" button). It is intentionally not exposed to API keys — API-key-authenticated bulk export is on the Phase 3 roadmap.

Query params

ParamTypeRequiredDefaultDescription
formatcsv | jsonnocsvOutput format
scanIdstring (uuid)nolatestExport a specific scan. Omit to export the latest completed scan.
GET /api/sites/11111111-…/export?format=csv HTTP/1.1
Host: sitehealth.octagramlabs.com
Cookie: session=…

CSV response

Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="Acme_Inc-scan-aaaabbbb.csv"

Page URL,Page Path,Strategy,Performance,SEO,Accessibility,Best Practices,LCP (ms),CLS,INP (ms),FCP (ms),TTFB (ms),CrUX LCP,CrUX CLS,CrUX INP,Has CrUX Data,Top Opportunities
https://acme.com/,/,mobile,82,95,91,100,2300,0.08,180,1200,420,2100,0.05,150,yes,"Eliminate render-blocking resources (520ms); Defer offscreen images (310ms)"

JSON response

[
{
"pageUrl": "https://acme.com/",
"pagePath": "/",
"strategy": "mobile",
"scores": { "performance": 82, "seo": 95, "accessibility": 91, "bestPractices": 100 },
"coreWebVitals": { "lcp": 2300, "cls": 0.08, "inp": 180, "fcp": 1200, "ttfb": 420 },
"crux": { "lcp": 2100, "cls": 0.05, "inp": 150, "hasCruxData": true },
"opportunities": [{ "title": "Eliminate render-blocking resources", "savingsMs": 520 }],
"thirdPartyScripts": [{ "domain": "www.googletagmanager.com", "transferSize": 45000, "blockingTime": 220 }],
"a11yIssues": [{ "id": "color-contrast", "title": "…", "score": 0 }]
}
]

Status codes

CodeWhen
200File returned
401No session cookie
404Site not found, or no completed scans for the site

Planned (Phase 3)

These endpoints are on the roadmap but not yet shipped. Interface shown is indicative.

POST /api/external/scan (planned)

Trigger a scan remotely — same as clicking "Scan now" in the UI.

POST /api/external/scan
Authorization: Bearer wsh_…
Content-Type: application/json

{ "siteId": "…", "strategy": "both" }

Response 202: { "scanId": "…", "status": "queued" }.

Rate-limited to 5 scans per user per hour. Deduped against in-flight scans (returns 409 with error: "SCAN_ALREADY_ACTIVE").

warning

Until Phase 3 ships, triggering a scan programmatically requires posting a fake site_publish webhook or kicking a manual scan from the dashboard. Do not rely on POST /api/external/scan existing today.

POST /api/external/webhooks/subscribe (planned)

Outgoing webhooks (Zapier / Make / n8n) for scan completion, regression detection, and budget violations.

Rate limits

EndpointLimit
POST /api/external/budget-checkunlimited (read-only)
GET /api/sites/{siteId}/exportunlimited
POST /api/external/scan (planned)5 per user per hour

Next: Webhooks →