API Documentation
Use the API to read your sites, scans, and findings. Trigger scans and get alerts when things change. Works on Pro and Studio plans.
All calls use JSON. Each response has a request ID you can use for support. Set up takes less than a minute.
Quickstart
Get your first scan result in 60 seconds. Create an API key in Settings, then run these three commands:
1. List your sites
curl -H "Authorization: Bearer sc_live_YOUR_KEY" \ https://sitecurl.com/api/v1/sites
2. Trigger a scan
curl -X POST \ -H "Authorization: Bearer sc_live_YOUR_KEY" \ https://sitecurl.com/api/v1/sites/SITE_ID/scans
3. Get findings
curl -H "Authorization: Bearer sc_live_YOUR_KEY" \ https://sitecurl.com/api/v1/scans/SCAN_ID/findings
Every response has X-Request-ID and X-RateLimit-* headers so you can track usage.
Authentication
Every request needs a Bearer token in the Authorization header:
Authorization: Bearer sc_live_...
Create your API key in Settings. Open the API tab to get started.
Short-lived access tokens
For scripts and agents, trade your API key for a token that expires in one hour. Keep the key in a secret manager and send only the token on each request:
curl -X POST https://sitecurl.com/api/v1/tokens \
-H "Authorization: Bearer sc_live_YOUR_KEY"
{ "access_token": "sc_tok_...", "token_type": "Bearer", "expires_in": 3600 }
Use sc_tok_ exactly like a key on any endpoint. It carries the same account access and rate limit as the key that created it.
Two things to know. An access token cannot create another access token, so a leaked one expires within the hour. Revoking the key immediately invalidates every token it created, and that is the only way to revoke one, since tokens cannot be revoked individually.
Rate limits
| Plan | Requests per hour |
|---|---|
| Pro | 100 |
| Studio | 1,000 |
Each response includes these headers:
X-Request-ID: a unique ID for each request (send your own or we make one)X-RateLimit-Limit: max requests per hourX-RateLimit-Remaining: how many you have leftX-RateLimit-Reset: when the window resets (Unix time)Retry-After: seconds to wait (only on 429 responses)
Sites
List sites
GET /api/v1/sites
Returns all sites on your account.
Pass page (default 1) and per_page (default 25, max 100) to paginate.
{
"data": [
{
"id": "uuid",
"name": "example.com",
"url": "https://example.com/",
"status": "active",
"scan_frequency": "weekly",
"last_scanned_at": "2026-03-18T12:00:00Z",
"created_at": "2026-01-15T10:30:00Z"
}
],
"meta": { "page": 1, "per_page": 25, "has_more": false }
}
Get site
GET /api/v1/sites/:id
Returns one site and its latest scan.
Scans
List scans
GET /api/v1/sites/:site_id/scans
Get scan
GET /api/v1/scans/:id
Response fields:
id,site_id,status,scorechecks_total,passed_count,critical_count,warning_countpages_scanned,delta_fixed_count,delta_regressed_countstarted_at,completed_at
Trigger scan
POST /api/v1/sites/:site_id/scans
Returns 202. The scan runs in the background. Use webhooks or poll the scan endpoint to know when it is done.
Returns 409 if a scan is still running. Returns 429 if you hit the daily cap.
Findings
List findings for a scan
GET /api/v1/scans/:scan_id/findings
Optional filters:
category: one of the API filter keys listed below
| API key | Category |
|---|---|
seo | SEO |
performance | speed |
security | security |
accessibility | accessibility |
technical | technical health |
availability | uptime |
ai_search_readiness | AI readiness |
severity: critical, warning, infopassed: true or falsecheck_key: filter by specific check keypage_url: filter by page URLpage(default 1),per_page(default 50, max 200)
Each finding has its category, severity, message, fix, page URL, and proof.
Reports
Get report
GET /api/v1/reports/:token
Returns the report token, site, scan, and expiry date.
Intelligence
Trends and changes you can only see over time.
Score trends
GET /api/v1/sites/:id/intelligence/trends?limit=20
Returns past scores, oldest first.
Regressions
GET /api/v1/sites/:id/intelligence/regressions
Checks that broke since the last scan, plus new issues.
Improvements
GET /api/v1/sites/:id/intelligence/improvements
Checks that were fixed since the last scan.
Priority inbox
GET /api/v1/intelligence/priority
Your top 5 action items across all sites. Covers score drops, new issues, quick wins, and stale scans.
Lead-capture scans
Studio plan only. Scan an arbitrary URL and email a whitelabeled "top 5 issues" partial report to a recipient, built for a lead-capture form on your own site, not the SiteCurl dashboard. No site is added to your account. Any form builder, plugin or automation tool that can send an authenticated POST request with your API key can call it.
POST /api/v1/lead_scans
{ "url": "https://...", "recipient_email": "[email protected]" }
Set the report's call-to-action link under Settings › Report branding › "Lead-capture report link": it points at your own site, not a SiteCurl-hosted report, since the point of a partial report is that the full findings stay yours to deliver. A duplicate (url, recipient_email) submission within 10 minutes returns the original scan instead of running again.
Example request
curl -X POST \
-H "Authorization: Bearer sc_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","recipient_email":"[email protected]"}' \
https://sitecurl.com/api/v1/lead_scans
The API answers 202 with a scan id right away and emails the report when the scan finishes. It accepts a JSON body or a form-encoded one, and a bare domain like example.com works as well as a full URL. If the site can't be scanned (unreachable, blocked, or a private address), the visitor gets no email. Instead you get an email with their address and the reason, so you can follow up yourself. Turn it off under Settings, Notifications.
Connecting a form
Any tool that can send an authenticated POST works. In the WPForms Webhooks add-on, choose POST and the JSON format, add a request header Authorization with the value Bearer sc_live_YOUR_KEY, and map url and recipient_email in the request body to your form's website and email fields. In Webhooks by Zapier, use a POST action with the JSON payload type and the same header and keys.
Webhooks
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/webhook_endpoints |
List all endpoints |
POST |
/api/v1/webhook_endpoints |
Create endpoint |
PATCH |
/api/v1/webhook_endpoints/:id |
Update endpoint |
DELETE |
/api/v1/webhook_endpoints/:id |
Delete endpoint |
Create request body
{ "url": "https://...", "events": ["scan_completed"] }
Available events: scan_completed, score_dropped, critical_regression, site_unreachable.
Payload headers
X-SiteCurl-Event: event typeX-SiteCurl-Signature: HMAC-SHA256 signature (v1=<hex>)X-SiteCurl-Timestamp: Unix timestamp
To check it: sign "#{timestamp}.#{body}" with your secret key.
Signature verification examples
Ruby
timestamp = request.headers["X-SiteCurl-Timestamp"]
signature = request.headers["X-SiteCurl-Signature"]
body = request.body.read
expected = "v1=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{body}")
unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)
head :unauthorized and return
end
Node.js
const crypto = require("crypto");
function verify(secret, timestamp, body, signature) {
const expected = "v1=" + crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(signature)
);
}
Python
import hmac, hashlib
def verify(secret, timestamp, body, signature):
expected = "v1=" + hmac.new(
secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Example: scan_completed
{
"event": "scan_completed",
"site_id": "uuid",
"scan_id": "uuid",
"score": 85,
"checks_total": 80,
"passed_count": 68,
"critical_count": 2,
"warning_count": 10,
"report_token": "abc123..."
}
Example: score_dropped
{
"event": "score_dropped",
"site_id": "uuid",
"scan_id": "uuid",
"current_score": 72,
"previous_score": 85,
"drop": 13
}
Example: critical_regression
Fires on a new critical OR warning-severity regression. critical_checks stays critical-only for backward compatibility; severities carries every flagged check with its actual severity.
{
"event": "critical_regression",
"site_id": "uuid",
"scan_id": "uuid",
"critical_checks": ["https_redirect"],
"severities": [
{ "check_key": "https_redirect", "severity": "critical" },
{ "check_key": "meta_description", "severity": "warning" }
]
}
Example: site_unreachable
Sent once per outage, when a scheduled scan could not reach the site and a second check 15 minutes later could not reach it either.
{
"event": "site_unreachable",
"site_id": "uuid",
"scan_id": "uuid",
"url": "https://example.com",
"cause": "dns"
}
cause is one of dns, connection, timeout or tls.
MCP (AI tools)
Connect AI coding tools like Claude Code, Cursor, or Windsurf to SiteCurl. Uses the same API key.
Endpoint
POST /api/v1/mcp
Speaks JSON-RPC 2.0. Three methods: initialize, tools/list, tools/call. Supports Streamable HTTP transport: send Accept: text/event-stream to receive SSE-formatted responses.
Setup
Add to your Claude Code or Cursor settings:
{
"mcpServers": {
"sitecurl": {
"url": "https://sitecurl.com/api/v1/mcp",
"headers": {
"Authorization": "Bearer sc_live_..."
}
}
}
}
Available tools
| Tool | Description |
|---|---|
sitecurl_list_sites |
List all monitored websites |
sitecurl_get_site |
Site details with latest scan |
sitecurl_scan |
Trigger a new scan |
sitecurl_scan_status |
Check status of latest scan |
sitecurl_findings |
Scan findings (filterable) |
sitecurl_score |
Current health score |
sitecurl_trends |
Score history over time |
sitecurl_priority |
Top action items across all sites |
sitecurl_regressions |
Checks that broke since last scan |
sitecurl_improvements |
Checks fixed since last scan |
sitecurl_add_site |
Add a website. Scheduled scans default to weekly on an active Pro, Studio or trial plan and to manual otherwise; pass scan_frequency to choose |
sitecurl_search_findings |
Search open findings across every site at once, grouped by site |
CI/CD integration
Block deploys when your site has critical audit findings. Install the @sitecurl/mcp npm package and run sitecurl-check in your pipeline.
Install
npm install -g @sitecurl/mcp
Usage
sitecurl-check https://example.com --fail-on=critical
Triggers a scan, waits for it to finish, then exits with code 0 (pass) or 1 (findings above your threshold). Set the API key via the SITECURL_API_KEY environment variable.
Exit codes
| Code | Meaning |
|---|---|
| 0 | No findings at or above threshold |
| 1 | Findings found, check failed |
| 2 | Scan failed or timed out |
| 3 | Setup error (bad key, missing site) |
GitHub Actions
- name: SiteCurl Audit
run: npx @sitecurl/mcp check https://example.com --fail-on=critical
env:
SITECURL_API_KEY: ${{ secrets.SITECURL_API_KEY }}
GitLab CI
sitecurl-audit:
script:
- npx @sitecurl/mcp check https://example.com --fail-on=critical
variables:
SITECURL_API_KEY: $SITECURL_API_KEY
Error responses
Errors come back as JSON with a message, code, and request ID:
{
"error": "Rate limit exceeded",
"error_code": "RATE_LIMIT_EXCEEDED",
"request_id": "a1b2c3d4-..."
}
| Status | Error code | Meaning |
|---|---|---|
| 401 | INVALID_API_KEY |
Invalid or missing API key |
| 403 | PLAN_REQUIRED |
Plan does not include API or webhook access |
| 403 | LONG_LIVED_KEY_REQUIRED |
Use an API key, not an access token, to create a token |
| 404 | NOT_FOUND |
Resource not found |
| 409 | SCAN_IN_PROGRESS |
A scan is already running for this site |
| 422 | VALIDATION_ERROR |
Invalid request parameters |
| 429 | RATE_LIMIT_EXCEEDED |
Hourly rate limit exceeded (includes Retry-After header) |
| 429 | QUOTA_EXCEEDED |
Daily scan quota exceeded |
Machine-readable spec
Download the full spec at /openapi.yaml (OpenAPI 3.1.0).
Questions?
Get in touch if anything is unclear.