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.comAuthentication
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_KEYMissing 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>" }500. Endpoint docs below list intended-but-not-yet-guaranteed cases separately from confirmed ones.Health checks
/healthLiveness probe. No auth, not rate-limited beyond infrastructure defaults.
{ "status": "ok", "uptime": 1234.5 }/readyReadiness probe — checks the database and Redis are reachable (1s timeout each).
{ "status": "ready" }{ "status": "not_ready", "checks": { "db": false, "redis": true } }List projects
/v1/projectsLists 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
/v1/scans/triggerQueues 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. |
| 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": "[email protected]"
}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": "[email protected]"
}'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
/v1/scans/:id/statusPoll 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": "[email protected]",
"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 |
| 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. |