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
| Field | Type | Required | Description |
|---|---|---|---|
siteId | string (uuid) | yes | Site 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
| Field | Type | Description |
|---|---|---|
pass | boolean | true if no violations. Use this as the CI gate. |
site | string | Site name |
scores | object | { health, performance, seo, accessibility, bestPractices } — 0–100 |
violations | array | Each: { metric, threshold, actual, operator } |
scanId | string (uuid) | The scan the check was run against |
scannedAt | ISO timestamp | When 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
| Code | When |
|---|---|
200 | Check ran (including pass: false). Also returned if the site has no completed scans yet (pass: false, no scores). |
400 | Body was not valid JSON |
401 | Missing or invalid API key |
404 | Site not found or not owned by the API key's user |
422 | siteId 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.
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
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
format | csv | json | no | csv | Output format |
scanId | string (uuid) | no | latest | Export 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
| Code | When |
|---|---|
200 | File returned |
401 | No session cookie |
404 | Site 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").
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
| Endpoint | Limit |
|---|---|
POST /api/external/budget-check | unlimited (read-only) |
GET /api/sites/{siteId}/export | unlimited |
POST /api/external/scan (planned) | 5 per user per hour |
Next: Webhooks →