GitHub Actions (budget check)
Fail your CI pipeline when a Webflow site's performance regresses below its configured budget. Site Health exposes a single-purpose /api/external/budget-check endpoint that returns pass: true or pass: false plus a violation list — perfect for a CI gate.
How it works
Site Health evaluates the latest completed scan against the site's budget and alert rules:
- Performance budget (
budgetPerformance) — minimum acceptable Performance score - LCP budget (
budgetLcp) — maximum acceptable average LCP in milliseconds - CLS budget (
budgetCls) — maximum acceptable average CLS ratio - Alert rules — custom per-metric thresholds (
performance,seo,accessibility,bestPractices) withlt/gtoperators
If any threshold is violated, the endpoint returns pass: false and the violating metrics. If all pass, you get pass: true.
Step 1 — Create an API key
In Site Health, go to Settings → API Keys → Create API Key and name it GitHub Actions. Copy the raw key once — you won't see it again.
Step 2 — Save it as a GitHub secret
- In your GitHub repo: Settings → Secrets and variables → Actions → New repository secret.
- Name:
SITE_HEALTH_API_KEY. - Value: the raw key from Step 1.
- Click Add secret.
You'll also want your site ID (UUID). Find it in Site Health — open the site's detail page and copy the ID from the URL (/sites/<siteId>).
Step 3 — Add the workflow
Create .github/workflows/performance-check.yml:
name: Performance Budget Check
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check Site Health budget
run: |
response=$(curl -s -w "\n%{http_code}" -X POST \
https://sitehealth.octagramlabs.com/api/external/budget-check \
-H "Authorization: Bearer ${{ secrets.SITE_HEALTH_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"siteId": "YOUR_SITE_ID"}')
body=$(echo "$response" | head -n -1)
status=$(echo "$response" | tail -n 1)
echo "HTTP $status"
echo "$body" | jq .
pass=$(echo "$body" | jq -r '.pass')
if [ "$pass" != "true" ]; then
echo "::error::Performance budget check failed"
exit 1
fi
Replace YOUR_SITE_ID with your site's UUID. Commit and push — the next push will run the job.
Response format
A passing response:
{
"pass": true,
"site": "studio-brava",
"scores": {
"health": 88,
"performance": 92,
"seo": 96,
"accessibility": 89,
"bestPractices": 100
},
"violations": [],
"scanId": "8b3e7c12-...",
"scannedAt": "2026-04-17T09:12:44.128Z"
}
A failing response:
{
"pass": false,
"site": "studio-brava",
"scores": { "health": 71, "performance": 68, "seo": 96, "accessibility": 89, "bestPractices": 100 },
"violations": [
{ "metric": "performance", "threshold": 85, "actual": 68, "operator": "min" },
{ "metric": "lcp", "threshold": 2500, "actual": 3180, "operator": "max" }
],
"scanId": "8b3e7c12-...",
"scannedAt": "2026-04-17T09:12:44.128Z"
}
Each violation contains:
metric—performance,seo,accessibility,bestPractices,lcp, orclsthreshold— the configured budget or rule valueactual— the measured value from the latest scanoperator—min(budget requires at least),max(budget requires at most),lt(rule triggers when less than),gt(rule triggers when greater than)
Tuning the check
This endpoint reads the latest completed scan at the moment of the request — it does not trigger a new scan. For CI that deploys to Webflow:
- Configure Webflow's
site_publishwebhook so Site Health rescans after every publish. - In your workflow, add a small delay (or a polling loop) after deploy so the webhook-triggered scan has time to complete before the budget check runs.
Configure budgets in Site Health
Open the site's detail page → Settings → Budgets. Set:
- Minimum Performance score (e.g.,
85) - Maximum LCP in ms (e.g.,
2500) - Maximum CLS ratio (e.g.,
0.1) - Any additional alert rules (e.g.,
accessibility lt 90)
The budget-check endpoint uses these exact values.
Cross-link
To auto-scan on publish, add the Webflow publish webhook. To debug budget decisions interactively, use an MCP client — see Claude.ai connector or Claude Desktop and ask "why did studio-brava fail its budget?"
Troubleshooting
401 "Missing Authorization header" or "Invalid API key." Check that the secret SITE_HEALTH_API_KEY is set at the repo (not environment) level, or the job references the right environment. Also verify the header is Authorization: Bearer ${{ secrets.SITE_HEALTH_API_KEY }} — the word Bearer is required.
404 "Site not found." The site ID is wrong or belongs to a different Site Health account. Confirm in the URL bar of the site's detail page.
200 response with pass: false and an empty violations array. The site has no completed scans yet. The endpoint returns {"error": "No completed scans", "pass": false} with status 200 so CI fails safely. Trigger a scan from Site Health (or wait for the webhook) before running the workflow.
422 Unprocessable Entity. The body didn't match the schema. Ensure you're sending {"siteId": "..."} as valid JSON with Content-Type: application/json and that the ID is a UUID.
The check passes locally but fails in CI (or vice versa). Both calls hit the same endpoint — the data is identical. If they disagree, a new scan completed between calls. Look at scanId in the response to confirm which scan each saw.