REST API reference
The API-key-authenticated REST surface for scripts, CI/CD pipelines, and custom integrations. To drive scans from an AI assistant instead, see the MCP setup guide.
Base URL
Every endpoint below is relative to this base - e.g. POST /v1/scans/trigger
means POST https://api.flawpilot.com/v1/scans/trigger.
https://api.flawpilot.com
Authentication
Every endpoint except the two health checks requires a Bearer API key. Create and manage keys from the dashboard under Settings → API Keys. A key identifies a workspace (tenant) - there is no per-user auth on this surface, and the same key works for the MCP server too.
Authorization: Bearer YOUR_API_KEY
Missing or invalid keys get:
{ "statusCode": 401, "message": "Unauthorized" }
Rate limits
| Scope | Limit | Applies to |
|---|---|---|
| Per-tenant, general | 60 req / min | Every authenticated endpoint, combined |
| Per-tenant, trigger scan | 5 req / min | POST /v1/scans/trigger only (on top of the general limit) |
| Per-IP, scan status | 2000 req / min | GET /v1/scans/:id/status - limited by IP, not the general tenant limit, since the website's own progress UI also polls the same underlying service. |
Exceeding a limit returns 429 with a Retry-After header (seconds until the
window resets):
{ "error": "Too many requests. Please retry shortly." }
Error shape
Authenticated and validated endpoints that fail return a consistent shape. There
is no separate machine-readable error code - match on statusCode plus, if
needed, the exact message text documented per endpoint.
{ "statusCode": 400, "message": "<human-readable message>" }
A few scan-creation failures from the scanning backend (e.g. the target domain
failing DNS resolution, or the backend's own rate limit) are not yet mapped to
their intended status code and currently surface as a generic 500. The endpoint
docs below list intended-but-not-yet-guaranteed cases separately from confirmed
ones.
Health checks
Both health checks are unauthenticated.
GET /health
Liveness probe. No auth, not rate-limited beyond infrastructure defaults.
{ "status": "ok", "uptime": 1234.5 }
GET /ready
Readiness probe - checks the database and Redis are reachable (1s timeout each).
{ "status": "ready" }
{ "status": "not_ready", "checks": { "db": false, "redis": true } }
List projects
GET /v1/projects
Lists the tenant's projects. Use this to discover a projectId before triggering a
scan. Auth required; general tenant rate limit only.
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
includeArchived | boolean (string "true") | false | Include archived projects in the results. |
Response - 200
[
{ "projectId": "PRJ-3TV1T", "name": "Marketing Site", "status": "ACTIVE" }
]
| Field | Type | Description |
|---|---|---|
projectId | string | Short, stable identifier - pass to trigger_scan. Not a DB ID. |
name | string | Project display name. |
status | string | ACTIVE or ARCHIVED. |
Example
curl https://api.flawpilot.com/v1/projects \
-H "Authorization: Bearer YOUR_API_KEY"
Trigger a scan
POST /v1/scans/trigger
Queues a scan for a URL against one of the tenant's projects. Auth required; general limit and the stricter 5/min trigger-scan limit.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
projectId | string | Yes | From GET /v1/projects. null is rejected the same as omitting it. |
url | string | Yes | Must be http:// or https:// and include the protocol. null is rejected the same as omitting it. |
pillars | array of SECURITY | PERFORMANCE | INFRASTRUCTURE | SEO | No | Omitted, null, or [] = full scan across all four pillars. |
email | string (valid email) | No | Sends the report here on completion. To skip it, omit the field or send null or "" - all three are equivalent. Does not accept the literal "skip". |
With an email
{
"projectId": "PRJ-3TV1T",
"url": "https://example.com",
"pillars": ["SECURITY"],
"email": "jane@example.com"
}
Skipping the email - these three are equivalent:
{ "projectId": "PRJ-3TV1T", "url": "https://example.com", "email": null }
{ "projectId": "PRJ-3TV1T", "url": "https://example.com", "email": "" }
{ "projectId": "PRJ-3TV1T", "url": "https://example.com" }
Example
curl -X POST https://api.flawpilot.com/v1/scans/trigger \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"projectId": "PRJ-XXXXX",
"url": "https://example.com",
"pillars": ["SECURITY"],
"email": "jane@example.com"
}'
Response - 202 Accepted
{ "scanJobId": "b3f1...", "status": "QUEUED", "queuedAt": "2026-07-27T10:00:00.000Z" }
Errors (confirmed)
| Status | Cause |
|---|---|
400 | Request body failed validation (missing/invalid projectId, url, pillars, or email). |
400 | "This project is archived and cannot be modified!" |
404 | "Project not found!" - projectId doesn't exist or belongs to another tenant. |
429 | Tenant or trigger-scan-specific limit exceeded. |
Errors (intended, not yet guaranteed)
| Status | Cause |
|---|---|
400 | Target domain isn't DNS-resolvable ("We couldn't reach that site…"). |
429 | The scanning backend's own internal rate limit ("Scan limit reached…"). |
Scan status
GET /v1/scans/:id/status
Poll scan progress. Auth required - send your Bearer API key, same as every other endpoint here. Rate-limited per IP, not the general tenant limit. Responses are cached a few seconds, so polling every ~5s is fine.
This public route is always authenticated. The website's own progress UI polls a separate, unauthenticated version of the same underlying service for logged-in browser sessions - but this API route never accepts unauthenticated calls.
Response - 200 (non-terminal)
{
"status": "RUNNING",
"completed": 8,
"total": 20,
"startedAt": "2026-07-27T10:00:05.000Z",
"url": "https://example.com",
"email": "jane@example.com",
"categories": [
{
"pillar": "SECURITY",
"title": "Security",
"score": 62.5,
"max_score": 100,
"band": "ELEVATED_RISK",
"results": [ /* subcategory scores */ ]
}
]
}
Once status reaches a terminal value (COMPLETE, PARTIAL, or FAILED), the
response also includes:
| Field | Description |
|---|---|
completedAt | ISO-8601 completion time. |
band | PRODUCTION_READY | GROWTH_READY | ELEVATED_RISK | CRITICAL_RISK - see Risk bands. |
score_security, score_performance, score_infrastructure, score_seo | Per-pillar scores. score_total is not included on this API surface - it is present on the website's own status polling, but withheld from public API and MCP responses. |
shareToken / shortCode | Same value, two names. Treat as a bearer credential - anyone holding it can view the public report. |
slug | Present if a human-readable slug was generated for the share URL. |
reportLink | Full URL to the public report page, e.g. https://app.flawpilot.com/report/<shortCode>. |
Errors
| Status | Cause |
|---|---|
401 | "Unauthorized" - missing or invalid API key. |
404 | "Scan not found!" - id doesn't correspond to any scan. |